[WIP] Quotes and link previews

This commit is contained in:
Ali 2023-10-18 01:11:23 +04:00
parent 137c3d9101
commit af8474aca5
88 changed files with 2582 additions and 966 deletions

View file

@ -531,6 +531,7 @@ public enum ChatControllerSubject: Equatable {
public struct LinkOptions: Equatable {
public var messageText: String
public var messageEntities: [MessageTextEntity]
public var hasAlternativeLinks: Bool
public var replyMessageId: EngineMessage.Id?
public var replyQuote: String?
public var url: String
@ -541,6 +542,7 @@ public enum ChatControllerSubject: Equatable {
public init(
messageText: String,
messageEntities: [MessageTextEntity],
hasAlternativeLinks: Bool,
replyMessageId: EngineMessage.Id?,
replyQuote: String?,
url: String,
@ -550,6 +552,7 @@ public enum ChatControllerSubject: Equatable {
) {
self.messageText = messageText
self.messageEntities = messageEntities
self.hasAlternativeLinks = hasAlternativeLinks
self.replyMessageId = replyMessageId
self.replyQuote = replyQuote
self.url = url

View file

@ -7,11 +7,31 @@ public enum ChatHistoryInitialSearchLocation: Equatable {
case id(MessageId)
}
public struct MessageHistoryScrollToSubject: Equatable {
public var index: MessageHistoryAnchorIndex
public var quote: String?
public init(index: MessageHistoryAnchorIndex, quote: String?) {
self.index = index
self.quote = quote
}
}
public struct MessageHistoryInitialSearchSubject: Equatable {
public var location: ChatHistoryInitialSearchLocation
public var quote: String?
public init(location: ChatHistoryInitialSearchLocation, quote: String?) {
self.location = location
self.quote = quote
}
}
public enum ChatHistoryLocation: Equatable {
case Initial(count: Int)
case InitialSearch(location: ChatHistoryInitialSearchLocation, count: Int, highlight: Bool)
case InitialSearch(subject: MessageHistoryInitialSearchSubject, count: Int, highlight: Bool)
case Navigation(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, count: Int, highlight: Bool)
case Scroll(index: MessageHistoryAnchorIndex, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool)
case Scroll(subject: MessageHistoryScrollToSubject, anchorIndex: MessageHistoryAnchorIndex, sourceIndex: MessageHistoryAnchorIndex, scrollPosition: ListViewScrollPosition, animated: Bool, highlight: Bool)
}
public struct ChatHistoryLocationInput: Equatable {

View file

@ -41,15 +41,15 @@ public struct ChatInterfaceSelectionState: Codable, Equatable {
}
public struct ChatEditMessageState: Codable, Equatable {
public let messageId: EngineMessage.Id
public let inputState: ChatTextInputState
public let disableUrlPreview: String?
public let inputTextMaxLength: Int32?
public var messageId: EngineMessage.Id
public var inputState: ChatTextInputState
public var disableUrlPreviews: [String]
public var inputTextMaxLength: Int32?
public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreview: String?, inputTextMaxLength: Int32?) {
public init(messageId: EngineMessage.Id, inputState: ChatTextInputState, disableUrlPreviews: [String], inputTextMaxLength: Int32?) {
self.messageId = messageId
self.inputState = inputState
self.disableUrlPreview = disableUrlPreview
self.disableUrlPreviews = disableUrlPreviews
self.inputTextMaxLength = inputTextMaxLength
}
@ -68,7 +68,15 @@ public struct ChatEditMessageState: Codable, Equatable {
self.inputState = ChatTextInputState()
}
self.disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup")
if let disableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") {
self.disableUrlPreviews = disableUrlPreviews
} else {
if let disableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") {
self.disableUrlPreviews = [disableUrlPreview]
} else {
self.disableUrlPreviews = []
}
}
self.inputTextMaxLength = try? container.decodeIfPresent(Int32.self, forKey: "tl")
}
@ -82,20 +90,20 @@ public struct ChatEditMessageState: Codable, Equatable {
try container.encode(self.inputState, forKey: "is")
try container.encodeIfPresent(self.disableUrlPreview, forKey: "dup")
try container.encode(self.disableUrlPreviews, forKey: "dupl")
try container.encodeIfPresent(self.inputTextMaxLength, forKey: "tl")
}
public static func ==(lhs: ChatEditMessageState, rhs: ChatEditMessageState) -> Bool {
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreview == rhs.disableUrlPreview && lhs.inputTextMaxLength == rhs.inputTextMaxLength
return lhs.messageId == rhs.messageId && lhs.inputState == rhs.inputState && lhs.disableUrlPreviews == rhs.disableUrlPreviews && lhs.inputTextMaxLength == rhs.inputTextMaxLength
}
public func withUpdatedInputState(_ inputState: ChatTextInputState) -> ChatEditMessageState {
return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreview: self.disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength)
return ChatEditMessageState(messageId: self.messageId, inputState: inputState, disableUrlPreviews: self.disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength)
}
public func withUpdatedDisableUrlPreview(_ disableUrlPreview: String?) -> ChatEditMessageState {
return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreview: disableUrlPreview, inputTextMaxLength: self.inputTextMaxLength)
public func withUpdatedDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatEditMessageState {
return ChatEditMessageState(messageId: self.messageId, inputState: self.inputState, disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: self.inputTextMaxLength)
}
}
@ -278,7 +286,7 @@ public final class ChatInterfaceState: Codable, Equatable {
public let timestamp: Int32
public let composeInputState: ChatTextInputState
public let composeDisableUrlPreview: String?
public let composeDisableUrlPreviews: [String]
public let replyMessageSubject: ReplyMessageSubject?
public let forwardMessageIds: [EngineMessage.Id]?
public let forwardOptionsState: ChatInterfaceForwardOptionsState?
@ -326,7 +334,7 @@ public final class ChatInterfaceState: Codable, Equatable {
public init() {
self.timestamp = 0
self.composeInputState = ChatTextInputState()
self.composeDisableUrlPreview = nil
self.composeDisableUrlPreviews = []
self.replyMessageSubject = nil
self.forwardMessageIds = nil
self.forwardOptionsState = nil
@ -339,10 +347,10 @@ public final class ChatInterfaceState: Codable, Equatable {
self.inputLanguage = nil
}
public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreview: String?, replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) {
public init(timestamp: Int32, composeInputState: ChatTextInputState, composeDisableUrlPreviews: [String], replyMessageSubject: ReplyMessageSubject?, forwardMessageIds: [EngineMessage.Id]?, forwardOptionsState: ChatInterfaceForwardOptionsState?, editMessage: ChatEditMessageState?, selectionState: ChatInterfaceSelectionState?, messageActionsState: ChatInterfaceMessageActionsState, historyScrollState: ChatInterfaceHistoryScrollState?, mediaRecordingMode: ChatTextInputMediaRecordingButtonMode, silentPosting: Bool, inputLanguage: String?) {
self.timestamp = timestamp
self.composeInputState = composeInputState
self.composeDisableUrlPreview = composeDisableUrlPreview
self.composeDisableUrlPreviews = composeDisableUrlPreviews
self.replyMessageSubject = replyMessageSubject
self.forwardMessageIds = forwardMessageIds
self.forwardOptionsState = forwardOptionsState
@ -364,10 +372,13 @@ public final class ChatInterfaceState: Codable, Equatable {
} else {
self.composeInputState = ChatTextInputState()
}
if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") {
self.composeDisableUrlPreview = composeDisableUrlPreview
if let composeDisableUrlPreviews = try? container.decodeIfPresent([String].self, forKey: "dupl") {
self.composeDisableUrlPreviews = composeDisableUrlPreviews
} else if let composeDisableUrlPreview = try? container.decodeIfPresent(String.self, forKey: "dup") {
self.composeDisableUrlPreviews = [composeDisableUrlPreview]
} else {
self.composeDisableUrlPreview = nil
self.composeDisableUrlPreviews = []
}
if let replyMessageSubject = try? container.decodeIfPresent(ReplyMessageSubject.self, forKey: "replyMessageSubject") {
@ -422,11 +433,8 @@ public final class ChatInterfaceState: Codable, Equatable {
try container.encode(self.timestamp, forKey: "ts")
try container.encode(self.composeInputState, forKey: "is")
if let composeDisableUrlPreview = self.composeDisableUrlPreview {
try container.encode(composeDisableUrlPreview, forKey: "dup")
} else {
try container.encodeNil(forKey: "dup")
}
try container.encode(self.composeDisableUrlPreviews, forKey: "dup;")
if let replyMessageSubject = self.replyMessageSubject {
try container.encode(replyMessageSubject, forKey: "replyMessageSubject")
} else {
@ -472,7 +480,7 @@ public final class ChatInterfaceState: Codable, Equatable {
}
public static func ==(lhs: ChatInterfaceState, rhs: ChatInterfaceState) -> Bool {
if lhs.composeDisableUrlPreview != rhs.composeDisableUrlPreview {
if lhs.composeDisableUrlPreviews != rhs.composeDisableUrlPreviews {
return false
}
if let lhsForwardMessageIds = lhs.forwardMessageIds, let rhsForwardMessageIds = rhs.forwardMessageIds {
@ -506,11 +514,11 @@ public final class ChatInterfaceState: Codable, Equatable {
public func withUpdatedComposeInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
let updatedComposeInputState = inputState
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedComposeDisableUrlPreview(_ disableUrlPreview: String?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: disableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
public func withUpdatedComposeDisableUrlPreviews(_ disableUrlPreviews: [String]) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: disableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedEffectiveInputState(_ inputState: ChatTextInputState) -> ChatInterfaceState {
@ -522,19 +530,19 @@ public final class ChatInterfaceState: Codable, Equatable {
updatedComposeInputState = inputState
}
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: updatedComposeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: updatedEditMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedReplyMessageSubject(_ replyMessageSubject: ReplyMessageSubject?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedForwardMessageIds(_ forwardMessageIds: [EngineMessage.Id]?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedForwardOptionsState(_ forwardOptionsState: ChatInterfaceForwardOptionsState?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedSelectedMessages(_ messageIds: [EngineMessage.Id]) -> ChatInterfaceState {
@ -545,7 +553,7 @@ public final class ChatInterfaceState: Codable, Equatable {
for messageId in messageIds {
selectedIds.insert(messageId)
}
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withToggledSelectedMessages(_ messageIds: [EngineMessage.Id], value: Bool) -> ChatInterfaceState {
@ -560,39 +568,39 @@ public final class ChatInterfaceState: Codable, Equatable {
selectedIds.remove(messageId)
}
}
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: ChatInterfaceSelectionState(selectedIds: selectedIds), messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withoutSelectionState() -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: nil, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedTimestamp(_ timestamp: Int32) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedEditMessage(_ editMessage: ChatEditMessageState?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedMessageActionsState(_ f: (ChatInterfaceMessageActionsState) -> ChatInterfaceMessageActionsState) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: f(self.messageActionsState), historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedHistoryScrollState(_ historyScrollState: ChatInterfaceHistoryScrollState?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedMediaRecordingMode(_ mediaRecordingMode: ChatTextInputMediaRecordingButtonMode) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedSilentPosting(_ silentPosting: Bool) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: silentPosting, inputLanguage: self.inputLanguage)
}
public func withUpdatedInputLanguage(_ inputLanguage: String?) -> ChatInterfaceState {
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreview: self.composeDisableUrlPreview, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage)
return ChatInterfaceState(timestamp: self.timestamp, composeInputState: self.composeInputState, composeDisableUrlPreviews: self.composeDisableUrlPreviews, replyMessageSubject: self.replyMessageSubject, forwardMessageIds: self.forwardMessageIds, forwardOptionsState: self.forwardOptionsState, editMessage: self.editMessage, selectionState: self.selectionState, messageActionsState: self.messageActionsState, historyScrollState: self.historyScrollState, mediaRecordingMode: self.mediaRecordingMode, silentPosting: self.silentPosting, inputLanguage: inputLanguage)
}
public static func parse(_ state: OpaqueChatInterfaceState) -> ChatInterfaceState {

View file

@ -3014,7 +3014,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
strongSelf.interaction.dismissInput()
strongSelf.interaction.present(controller, nil)
} else if case let .messages(chatLocation, _, _) = playlistLocation {
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: EngineMessage.Tags.music)
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: EngineMessage.Tags.music)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
@ -3603,7 +3603,7 @@ public final class ChatListSearchShimmerNode: ASDisplayNode {
return nil
case .links:
var media: [EngineMedia] = []
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default)))))
media.append(.webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: 0, id: 0), content: .Loaded(TelegramMediaWebpageLoadedContent(url: "https://telegram.org", displayUrl: "https://telegram.org", hash: 0, type: nil, websiteName: "Telegram", title: "Telegram Telegram", text: "Telegram", embedUrl: nil, embedType: nil, embedSize: nil, duration: nil, author: nil, isMediaLargeByDefault: nil, image: nil, file: nil, story: nil, attributes: [], instantPage: nil)))))
let message = EngineMessage(
stableId: 0,
stableVersion: 0,

View file

@ -307,6 +307,10 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
}
case _ as TelegramMediaGiveaway:
messageText = strings.Message_Giveaway
case let webpage as TelegramMediaWebpage:
if messageText.isEmpty, case let .Loaded(content) = webpage.content {
messageText = content.displayUrl
}
default:
break
}

View file

@ -324,6 +324,20 @@ public final class ChatPresentationInterfaceState: Equatable {
}
}
public struct UrlPreview: Equatable {
public var url: String
public var webPage: TelegramMediaWebpage
public var positionBelowText: Bool
public var largeMedia: Bool?
public init(url: String, webPage: TelegramMediaWebpage, positionBelowText: Bool, largeMedia: Bool?) {
self.url = url
self.webPage = webPage
self.positionBelowText = positionBelowText
self.largeMedia = largeMedia
}
}
public let interfaceState: ChatInterfaceState
public let chatLocation: ChatLocation
public let renderedPeer: RenderedPeer?
@ -350,8 +364,8 @@ public final class ChatPresentationInterfaceState: Equatable {
public let slowmodeState: ChatSlowmodeState?
public let chatHistoryState: ChatHistoryNodeHistoryState?
public let botStartPayload: String?
public let urlPreview: (String, TelegramMediaWebpage)?
public let editingUrlPreview: (String, TelegramMediaWebpage)?
public let urlPreview: UrlPreview?
public let editingUrlPreview: UrlPreview?
public let search: ChatSearchData?
public let searchQuerySuggestionResult: ChatPresentationInputQueryResult?
public let presentationReady: Bool
@ -466,7 +480,7 @@ public final class ChatPresentationInterfaceState: Equatable {
self.translationState = nil
}
public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: (String, TelegramMediaWebpage)?, editingUrlPreview: (String, TelegramMediaWebpage)?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?) {
public init(interfaceState: ChatInterfaceState, chatLocation: ChatLocation, renderedPeer: RenderedPeer?, isNotAccessible: Bool, explicitelyCanPinMessages: Bool, contactStatus: ChatContactStatus?, hasBots: Bool, isArchived: Bool, inputTextPanelState: ChatTextInputPanelState, editMessageState: ChatEditInterfaceMessageState?, recordedMediaPreview: ChatRecordedMediaPreview?, inputQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult], inputMode: ChatInputMode, titlePanelContexts: [ChatTitlePanelContext], keyboardButtonsMessage: Message?, pinnedMessageId: MessageId?, pinnedMessage: ChatPinnedMessage?, peerIsBlocked: Bool, peerIsMuted: Bool, peerDiscussionId: PeerId?, peerGeoLocation: PeerGeoLocation?, callsAvailable: Bool, callsPrivate: Bool, slowmodeState: ChatSlowmodeState?, chatHistoryState: ChatHistoryNodeHistoryState?, botStartPayload: String?, urlPreview: UrlPreview?, editingUrlPreview: UrlPreview?, search: ChatSearchData?, searchQuerySuggestionResult: ChatPresentationInputQueryResult?, presentationReady: Bool, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, autoremoveTimeout: Int32?, subject: ChatControllerSubject?, peerNearbyData: ChatPeerNearbyData?, greetingData: ChatGreetingData?, pendingUnpinnedAllMessages: Bool, activeGroupCallInfo: ChatActiveGroupCallInfo?, hasActiveGroupCall: Bool, importState: ChatPresentationImportState?, reportReason: ReportReason?, showCommands: Bool, hasBotCommands: Bool, showSendAsPeers: Bool, sendAsPeers: [SendAsPeer]?, botMenuButton: BotMenuButton, showWebView: Bool, currentSendAsPeerId: PeerId?, copyProtectionEnabled: Bool, hasPlentyOfMessages: Bool, isPremium: Bool, premiumGiftOptions: [CachedPremiumGiftOption], suggestPremiumGift: Bool, forceInputCommandsHidden: Bool, voiceMessagesAvailable: Bool, customEmojiAvailable: Bool, threadData: ThreadData?, isGeneralThreadClosed: Bool?, translationState: ChatPresentationTranslationState?) {
self.interfaceState = interfaceState
self.chatLocation = chatLocation
self.renderedPeer = renderedPeer
@ -622,20 +636,14 @@ public final class ChatPresentationInterfaceState: Equatable {
return false
}
if let lhsUrlPreview = lhs.urlPreview, let rhsUrlPreview = rhs.urlPreview {
if lhsUrlPreview.0 != rhsUrlPreview.0 {
return false
}
if !lhsUrlPreview.1.isEqual(to: rhsUrlPreview.1) {
if lhsUrlPreview != rhsUrlPreview {
return false
}
} else if (lhs.urlPreview != nil) != (rhs.urlPreview != nil) {
return false
}
if let lhsEditingUrlPreview = lhs.editingUrlPreview, let rhsEditingUrlPreview = rhs.editingUrlPreview {
if lhsEditingUrlPreview.0 != rhsEditingUrlPreview.0 {
return false
}
if !lhsEditingUrlPreview.1.isEqual(to: rhsEditingUrlPreview.1) {
if lhsEditingUrlPreview != rhsEditingUrlPreview {
return false
}
} else if (lhs.editingUrlPreview != nil) != (rhs.editingUrlPreview != nil) {
@ -869,11 +877,11 @@ public final class ChatPresentationInterfaceState: Equatable {
return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState)
}
public func updatedUrlPreview(_ urlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState {
public func updatedUrlPreview(_ urlPreview: UrlPreview?) -> ChatPresentationInterfaceState {
return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: urlPreview, editingUrlPreview: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState)
}
public func updatedEditingUrlPreview(_ editingUrlPreview: (String, TelegramMediaWebpage)?) -> ChatPresentationInterfaceState {
public func updatedEditingUrlPreview(_ editingUrlPreview: UrlPreview?) -> ChatPresentationInterfaceState {
return ChatPresentationInterfaceState(interfaceState: self.interfaceState, chatLocation: self.chatLocation, renderedPeer: self.renderedPeer, isNotAccessible: self.isNotAccessible, explicitelyCanPinMessages: self.explicitelyCanPinMessages, contactStatus: self.contactStatus, hasBots: self.hasBots, isArchived: self.isArchived, inputTextPanelState: self.inputTextPanelState, editMessageState: self.editMessageState, recordedMediaPreview: self.recordedMediaPreview, inputQueryResults: self.inputQueryResults, inputMode: self.inputMode, titlePanelContexts: self.titlePanelContexts, keyboardButtonsMessage: self.keyboardButtonsMessage, pinnedMessageId: self.pinnedMessageId, pinnedMessage: self.pinnedMessage, peerIsBlocked: self.peerIsBlocked, peerIsMuted: self.peerIsMuted, peerDiscussionId: self.peerDiscussionId, peerGeoLocation: self.peerGeoLocation, callsAvailable: self.callsAvailable, callsPrivate: self.callsPrivate, slowmodeState: self.slowmodeState, chatHistoryState: self.chatHistoryState, botStartPayload: self.botStartPayload, urlPreview: self.urlPreview, editingUrlPreview: editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, presentationReady: self.presentationReady, chatWallpaper: self.chatWallpaper, theme: self.theme, strings: self.strings, dateTimeFormat: self.dateTimeFormat, nameDisplayOrder: self.nameDisplayOrder, limitsConfiguration: self.limitsConfiguration, fontSize: self.fontSize, bubbleCorners: self.bubbleCorners, accountPeerId: self.accountPeerId, mode: self.mode, hasScheduledMessages: self.hasScheduledMessages, autoremoveTimeout: self.autoremoveTimeout, subject: self.subject, peerNearbyData: self.peerNearbyData, greetingData: self.greetingData, pendingUnpinnedAllMessages: self.pendingUnpinnedAllMessages, activeGroupCallInfo: self.activeGroupCallInfo, hasActiveGroupCall: self.hasActiveGroupCall, importState: self.importState, reportReason: self.reportReason, showCommands: self.showCommands, hasBotCommands: self.hasBotCommands, showSendAsPeers: self.showSendAsPeers, sendAsPeers: self.sendAsPeers, botMenuButton: self.botMenuButton, showWebView: self.showWebView, currentSendAsPeerId: self.currentSendAsPeerId, copyProtectionEnabled: self.copyProtectionEnabled, hasPlentyOfMessages: self.hasPlentyOfMessages, isPremium: self.isPremium, premiumGiftOptions: self.premiumGiftOptions, suggestPremiumGift: self.suggestPremiumGift, forceInputCommandsHidden: self.forceInputCommandsHidden, voiceMessagesAvailable: self.voiceMessagesAvailable, customEmojiAvailable: self.customEmojiAvailable, threadData: self.threadData, isGeneralThreadClosed: self.isGeneralThreadClosed, translationState: self.translationState)
}

View file

@ -2226,6 +2226,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
case custom(ContextControllerItemsContent)
}
public var id: AnyHashable?
public var content: Content
public var context: AccountContext?
public var reactionItems: [ReactionContextItem]
@ -2238,6 +2239,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
public var dismissed: (() -> Void)?
public init(
id: AnyHashable? = nil,
content: Content,
context: AccountContext? = nil,
reactionItems: [ReactionContextItem] = [],
@ -2249,6 +2251,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
tipSignal: Signal<Tip?, NoError>? = nil,
dismissed: (() -> Void)? = nil
) {
self.id = id
self.content = content
self.context = context
self.animationCache = animationCache
@ -2262,6 +2265,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
}
public init() {
self.id = nil
self.content = .list([])
self.context = nil
self.reactionItems = []

View file

@ -41,6 +41,7 @@ public protocol ContextControllerActionsStackItem: AnyObject {
requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void
) -> ContextControllerActionsStackItemNode
var id: AnyHashable? { get }
var tip: ContextController.Tip? { get }
var tipSignal: Signal<ContextController.Tip?, NoError>? { get }
var reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)? { get }
@ -833,6 +834,7 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
}
}
let id: AnyHashable?
private let items: [ContextMenuItem]
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
@ -840,12 +842,14 @@ final class ContextControllerActionsListStackItem: ContextControllerActionsStack
let dismissed: (() -> Void)?
init(
id: AnyHashable?,
items: [ContextMenuItem],
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
) {
self.id = id
self.items = items
self.reactionItems = reactionItems
self.tip = tip
@ -928,6 +932,7 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
}
}
let id: AnyHashable?
private let content: ContextControllerItemsContent
let reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?
let tip: ContextController.Tip?
@ -935,12 +940,14 @@ final class ContextControllerActionsCustomStackItem: ContextControllerActionsSta
let dismissed: (() -> Void)?
init(
id: AnyHashable?,
content: ContextControllerItemsContent,
reactionItems: (context: AccountContext, reactionItems: [ReactionContextItem], selectedReactionItems: Set<MessageReaction.Reaction>, animationCache: AnimationCache, getEmojiContent: ((AnimationCache, MultiAnimationRenderer) -> Signal<EmojiPagerContentComponent, NoError>)?)?,
tip: ContextController.Tip?,
tipSignal: Signal<ContextController.Tip?, NoError>?,
dismissed: (() -> Void)?
) {
self.id = id
self.content = content
self.reactionItems = reactionItems
self.tip = tip
@ -970,11 +977,11 @@ func makeContextControllerActionsStackItem(items: ContextController.Items) -> [C
}
switch items.content {
case let .list(listItems):
return [ContextControllerActionsListStackItem(items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
return [ContextControllerActionsListStackItem(id: items.id, items: listItems, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
case let .twoLists(listItems1, listItems2):
return [ContextControllerActionsListStackItem(items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)]
return [ContextControllerActionsListStackItem(id: items.id, items: listItems1, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: items.dismissed), ContextControllerActionsListStackItem(id: nil, items: listItems2, reactionItems: nil, tip: nil, tipSignal: nil, dismissed: nil)]
case let .custom(customContent):
return [ContextControllerActionsCustomStackItem(content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
return [ContextControllerActionsCustomStackItem(id: items.id, content: customContent, reactionItems: reactionItems, tip: items.tip, tipSignal: items.tipSignal, dismissed: items.dismissed)]
}
}
@ -1084,6 +1091,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
final class ItemContainer: ASDisplayNode {
let getController: () -> ContextControllerProtocol?
let requestUpdate: (ContainedViewLayoutTransition) -> Void
let item: ContextControllerActionsStackItem
let node: ContextControllerActionsStackItemNode
let dimNode: ASDisplayNode
var tip: ContextController.Tip?
@ -1110,6 +1118,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
) {
self.getController = getController
self.requestUpdate = requestUpdate
self.item = item
self.node = item.node(
getController: getController,
requestDismiss: requestDismiss,
@ -1316,9 +1325,20 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
}
}
func replace(item: ContextControllerActionsStackItem, animated: Bool) {
func replace(item: ContextControllerActionsStackItem, animated: Bool?) {
var resolvedAnimated = false
if let animated {
resolvedAnimated = animated
} else {
if let id = item.id, let lastId = self.itemContainers.last?.item.id {
if id != lastId {
resolvedAnimated = true
}
}
}
for itemContainer in self.itemContainers {
if animated {
if resolvedAnimated {
self.dismissingItemContainers.append((itemContainer, false))
} else {
itemContainer.tipNode?.removeFromSupernode()
@ -1328,7 +1348,7 @@ final class ContextControllerActionsStackNode: ASDisplayNode {
self.itemContainers.removeAll()
self.navigationContainer.isNavigationEnabled = self.itemContainers.count > 1
self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: animated)
self.push(item: item, currentScrollingState: nil, positionLock: nil, animated: resolvedAnimated)
}
func push(item: ContextControllerActionsStackItem, currentScrollingState: CGFloat?, positionLock: CGFloat?, animated: Bool) {

View file

@ -484,7 +484,7 @@ final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextCo
}
}
func replaceItems(items: ContextController.Items, animated: Bool) {
func replaceItems(items: ContextController.Items, animated: Bool?) {
if case .twoLists = items.content {
let stackItems = makeContextControllerActionsStackItem(items: items)
self.actionsStackNode.replace(item: stackItems.first!, animated: animated)

View file

@ -16,7 +16,7 @@ enum ContextControllerPresentationNodeStateTransition {
protocol ContextControllerPresentationNode: ASDisplayNode {
var ready: Signal<Bool, NoError> { get }
func replaceItems(items: ContextController.Items, animated: Bool)
func replaceItems(items: ContextController.Items, animated: Bool?)
func pushItems(items: ContextController.Items)
func popItems()
func wantsDisplayBelowKeyboard() -> Bool

View file

@ -221,7 +221,7 @@ final class ContextSourceContainer: ASDisplayNode {
return
}
self.setItems(items: items, animated: false)
self.setItems(items: items, animated: nil)
self.actionsReady.set(.single(true))
}))
}
@ -273,7 +273,7 @@ final class ContextSourceContainer: ASDisplayNode {
}))
}
func setItems(items: ContextController.Items, animated: Bool) {
func setItems(items: ContextController.Items, animated: Bool?) {
self.presentationNode.replaceItems(items: items, animated: animated)
}

View file

@ -52,7 +52,7 @@ private func drawConnectingCorner(context: CGContext, color: UIColor, at point:
}
}
public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) {
public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, outerRadius: CGFloat, innerRadius: CGFloat, stroke: Bool = false, strokeWidth: CGFloat = 2.0, useModernPathCalculation: Bool) -> (CGPoint, UIImage?) {
if rects.isEmpty {
return (CGPoint(), nil)
}
@ -66,10 +66,15 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat,
bottomRight.y = max(bottomRight.y, rects[i].maxY)
}
topLeft.x -= inset
topLeft.y -= inset
bottomRight.x += inset * 2.0
bottomRight.y += inset * 2.0
var drawingInset = inset
if stroke {
drawingInset += 2.0
}
topLeft.x -= drawingInset
topLeft.y -= drawingInset
bottomRight.x += drawingInset * 2.0
bottomRight.y += drawingInset * 2.0
return (topLeft, generateImage(CGSize(width: bottomRight.x - topLeft.x, height: bottomRight.y - topLeft.y), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
@ -84,7 +89,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat,
if stroke {
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.setLineWidth(strokeWidth)
context.strokePath()
} else {
context.fillPath()
@ -212,7 +217,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat,
if stroke {
context.setStrokeColor(color.cgColor)
context.setLineWidth(2.0)
context.setLineWidth(strokeWidth)
context.strokePath()
} else {
context.fillPath()
@ -306,6 +311,8 @@ public final class LinkHighlightingNode: ASDisplayNode {
public var outerRadius: CGFloat = 4.0
public var inset: CGFloat = 2.0
public var useModernPathCalculation: Bool = false
public var borderOnly: Bool = false
public var strokeWidth: CGFloat = 1.0
private var _color: UIColor
public var color: UIColor {
@ -352,7 +359,7 @@ public final class LinkHighlightingNode: ASDisplayNode {
if self.rects.isEmpty {
self.imageNode.image = nil
}
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, useModernPathCalculation: self.useModernPathCalculation)
let (offset, image) = generateRectsImage(color: self.color, rects: self.rects, inset: self.inset, outerRadius: self.outerRadius, innerRadius: self.innerRadius, stroke: self.borderOnly, strokeWidth: self.strokeWidth, useModernPathCalculation: self.useModernPathCalculation)
if let image = image {
self.imageNode.image = image

View file

@ -453,7 +453,7 @@ public final class TextNodeLayout: NSObject {
for blockQuote in self.blockQuotes {
if lastLine.frame.intersects(blockQuote.frame) {
width = max(width, blockQuote.frame.maxX)
width = max(width, ceil(blockQuote.frame.maxX) + 2.0)
}
}
return width
@ -764,6 +764,17 @@ public final class TextNodeLayout: NSObject {
return nil
}
public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? {
if let attributedString = self.attributedString {
var range = NSRange()
let _ = attributedString.attribute(NSAttributedString.Key(rawValue: name), at: index, effectiveRange: &range)
if range.length != 0 {
return ((attributedString.string as NSString).substring(with: range), attributedString.string, range)
}
}
return nil
}
public func allAttributeRects(name: String) -> [(Any, CGRect)] {
guard let attributedString = self.attributedString else {
return []
@ -1064,6 +1075,10 @@ open class TextNode: ASDisplayNode {
return self.cachedLayout?.attributeSubstring(name: name, index: index)
}
public func attributeSubstringWithRange(name: String, index: Int) -> (String, String, NSRange)? {
return self.cachedLayout?.attributeSubstringWithRange(name: name, index: index)
}
public func attributeRects(name: String, at index: Int) -> [CGRect]? {
if let cachedLayout = self.cachedLayout {
return cachedLayout.lineAndAttributeRects(name: name, at: index)?.map { $0.1 }
@ -1341,6 +1356,9 @@ open class TextNode: ASDisplayNode {
}
}
size.width = ceil(size.width)
size.height = ceil(size.height)
let rawTextSize = size
size.width += insets.left + insets.right
size.height += insets.top + insets.bottom
@ -1992,7 +2010,8 @@ open class TextNode: ASDisplayNode {
}
for blockQuote in layout.blockQuotes {
let radius: CGFloat = 3.0
let radius: CGFloat = 4.0
let lineWidth: CGFloat = 3.0
var blockFrame = blockQuote.frame.offsetBy(dx: offset.x + 2.0, dy: offset.y)
blockFrame.size.width += 4.0
@ -2014,13 +2033,15 @@ open class TextNode: ASDisplayNode {
context.restoreGState()
context.resetClip()
let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: radius, height: blockFrame.height))
let lineFrame = CGRect(origin: CGPoint(x: blockFrame.minX, y: blockFrame.minY), size: CGSize(width: lineWidth, height: blockFrame.height))
context.move(to: CGPoint(x: lineFrame.minX, y: lineFrame.minY + radius))
context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.minY), tangent2End: CGPoint(x: lineFrame.minX + radius, y: lineFrame.minY), radius: radius)
context.addLine(to: CGPoint(x: lineFrame.minX + radius, y: lineFrame.maxY))
context.addArc(tangent1End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY), tangent2End: CGPoint(x: lineFrame.minX, y: lineFrame.maxY - radius), radius: radius)
context.closePath()
context.fillPath()
context.clip()
context.fill(lineFrame)
context.resetClip()
}
if let textShadowColor = layout.textShadowColor {

View file

@ -632,7 +632,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
let item: InstantPageItem
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil, displayOptions: .default)
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, isMediaLargeByDefault: nil, image: image, file: nil, story: nil, attributes: [], instantPage: nil)
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)

View file

@ -818,7 +818,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
strongSelf.displayNode.view.window?.endEditing(true)
strongSelf.present(controller, in: .window(.root))
} else if case let .messages(chatLocation, _, _) = playlistLocation {
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: MessageTags.music)
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: MessageTags.music)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View file

@ -261,8 +261,9 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
let peerId: PeerId = chatPeerId.peerId
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities):
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities):
let _ = replyHeader
let _ = replyMedia
let _ = replyToTopId
let _ = quoteText
let _ = quoteEntities
@ -282,8 +283,9 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
case let .messageService(_, id, _, chatPeerId, replyHeader, _, _, _):
if let replyHeader = replyHeader {
switch replyHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities):
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities):
let _ = replyHeader
let _ = replyMedia
let _ = replyToTopId
let _ = quoteText
let _ = quoteEntities
@ -302,48 +304,63 @@ func apiMessageAssociatedMessageIds(_ message: Api.Message) -> (replyIds: Refere
return nil
}
func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (Media?, Int32?, Bool?, Bool?) {
struct ParsedMessageWebpageAttributes {
var forceLargeMedia: Bool?
var isManuallyAdded: Bool
}
func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerId: PeerId) -> (media: Media?, expirationTimer: Int32?, nonPremium: Bool?, hasSpoiler: Bool?, webpageAttributes: ParsedMessageWebpageAttributes?) {
if let media = media {
switch media {
case let .messageMediaPhoto(flags, photo, ttlSeconds):
if let photo = photo {
if let mediaImage = telegramMediaImageFromApiPhoto(photo) {
return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0)
return (mediaImage, ttlSeconds, nil, (flags & (1 << 3)) != 0, nil)
}
} else {
return (TelegramMediaExpiredContent(data: .image), nil, nil, nil)
return (TelegramMediaExpiredContent(data: .image), nil, nil, nil, nil)
}
case let .messageMediaContact(phoneNumber, firstName, lastName, vcard, userId):
let contactPeerId: PeerId? = userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId))
let mediaContact = TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: contactPeerId, vCardData: vcard.isEmpty ? nil : vcard)
return (mediaContact, nil, nil, nil)
return (mediaContact, nil, nil, nil, nil)
case let .messageMediaGeo(geo):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil)
return (mediaMap, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil)
case let .messageMediaVenue(geo, title, address, provider, venueId, venueType):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: title, address: address, provider: provider, venueId: venueId, venueType: venueType, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil, heading: nil)
return (mediaMap, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil)
case let .messageMediaGeoLive(_, geo, heading, period, proximityNotificationRadius):
let mediaMap = telegramMediaMapFromApiGeoPoint(geo, title: nil, address: nil, provider: nil, venueId: nil, venueType: nil, liveBroadcastingTimeout: period, liveProximityNotificationRadius: proximityNotificationRadius, heading: heading)
return (mediaMap, nil, nil, nil)
return (mediaMap, nil, nil, nil, nil)
case let .messageMediaDocument(flags, document, _, ttlSeconds):
if let document = document {
if let mediaFile = telegramMediaFileFromApiDocument(document) {
return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0)
return (mediaFile, ttlSeconds, (flags & (1 << 3)) != 0, (flags & (1 << 4)) != 0, nil)
}
} else {
return (TelegramMediaExpiredContent(data: .file), nil, nil, nil)
return (TelegramMediaExpiredContent(data: .file), nil, nil, nil, nil)
}
case let .messageMediaWebPage(_, webpage):
case let .messageMediaWebPage(flags, webpage):
if let mediaWebpage = telegramMediaWebpageFromApiWebpage(webpage, url: nil) {
return (mediaWebpage, nil, nil, nil)
var webpageForceLargeMedia: Bool?
if (flags & (1 << 0)) != 0 {
webpageForceLargeMedia = true
} else if (flags & (1 << 1)) != 0 {
webpageForceLargeMedia = false
}
return (mediaWebpage, nil, nil, nil, ParsedMessageWebpageAttributes(
forceLargeMedia: webpageForceLargeMedia,
isManuallyAdded: (flags & (1 << 3)) != 0
))
}
case .messageMediaUnsupported:
return (TelegramMediaUnsupported(), nil, nil, nil)
return (TelegramMediaUnsupported(), nil, nil, nil, nil)
case .messageMediaEmpty:
break
case let .messageMediaGame(game):
return (TelegramMediaGame(apiGame: game), nil, nil, nil)
return (TelegramMediaGame(apiGame: game), nil, nil, nil, nil)
case let .messageMediaInvoice(flags, title, description, photo, receiptMsgId, currency, totalAmount, startParam, apiExtendedMedia):
var parsedFlags = TelegramMediaInvoiceFlags()
if (flags & (1 << 3)) != 0 {
@ -367,7 +384,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
}
extendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
case let .messageExtendedMedia(apiMedia):
let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId)
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, peerId)
if let media = media {
extendedMedia = .full(media: media)
} else {
@ -378,7 +395,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
extendedMedia = nil
}
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil)
return (TelegramMediaInvoice(title: title, description: description, photo: photo.flatMap(TelegramMediaWebFile.init), receiptMessageId: receiptMsgId.flatMap { MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: $0) }, currency: currency, totalAmount: totalAmount, startParam: startParam, extendedMedia: extendedMedia, flags: parsedFlags, version: TelegramMediaInvoice.lastVersion), nil, nil, nil, nil)
case let .messageMediaPoll(poll, results):
switch poll {
case let .poll(id, flags, question, answers, closePeriod, _):
@ -394,23 +411,23 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
} else {
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
}
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil)
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil)
}
case let .messageMediaDice(value, emoticon):
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil)
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil)
case let .messageMediaStory(flags, peerId, id, _):
let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil)
return (TelegramMediaStory(storyId: StoryId(peerId: peerId.peerId, id: id), isMention: isMention), nil, nil, nil, nil)
case let .messageMediaGiveaway(apiFlags, channels, countries, quantity, months, untilDate):
var flags: TelegramMediaGiveaway.Flags = []
if (apiFlags & (1 << 0)) != 0 {
flags.insert(.onlyNewSubscribers)
}
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil)
return (TelegramMediaGiveaway(flags: flags, channelPeerIds: channels.map { PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value($0)) }, countries: countries ?? [], quantity: quantity, months: months, untilDate: untilDate), nil, nil, nil, nil)
}
}
return (nil, nil, nil, nil)
return (nil, nil, nil, nil, nil)
}
func mediaAreaFromApiMediaArea(_ mediaArea: Api.MediaArea) -> MediaArea? {
@ -567,12 +584,12 @@ extension StoreMessage {
if let replyTo = replyTo {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities):
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities):
let isForumTopic = (flags & (1 << 3)) != 0
var quote: EngineMessageReplyQuote?
if let quoteText = quoteText {
quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []))
if quoteText != nil || replyMedia != nil {
quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media)
}
if let replyToMsgId = replyToMsgId {
@ -669,7 +686,7 @@ extension StoreMessage {
var consumableContent: (Bool, Bool)? = nil
if let media = media {
let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue {
medias.append(mediaValue)
@ -685,6 +702,14 @@ extension StoreMessage {
if let hasSpoiler = hasSpoiler, hasSpoiler {
attributes.append(MediaSpoilerMessageAttribute())
}
if mediaValue is TelegramMediaWebpage {
let leadingPreview = (flags & (1 << 27)) != 0
if let webpageAttributes {
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: leadingPreview, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded))
}
}
}
}
@ -841,10 +866,10 @@ extension StoreMessage {
if let replyTo = replyTo {
var threadMessageId: MessageId?
switch replyTo {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities):
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities):
var quote: EngineMessageReplyQuote?
if let quoteText = quoteText {
quote = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []))
if quoteText != nil || replyMedia != nil {
quote = EngineMessageReplyQuote(text: quoteText ?? "", entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []), media: textMediaAndExpirationTimerFromApiMedia(replyMedia, peerId).media)
}
if let replyToMsgId = replyToMsgId {

View file

@ -24,7 +24,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) ->
case let .webPagePending(flags, id, url, date):
let _ = flags
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Pending(date, url))
case let .webPage(_, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes):
case let .webPage(flags, id, url, displayUrl, hash, type, siteName, title, description, photo, embedUrl, embedType, embedWidth, embedHeight, duration, author, document, cachedPage, attributes):
var embedSize: PixelDimensions?
if let embedWidth = embedWidth, let embedHeight = embedHeight {
embedSize = PixelDimensions(width: embedWidth, height: embedHeight)
@ -56,7 +56,10 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) ->
if let cachedPage = cachedPage {
instantPage = InstantPage(apiPage: cachedPage)
}
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage, displayOptions: .default)))
let isMediaLargeByDefault = (flags & (1 << 13)) != 0
return TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.CloudWebpage, id: id), content: .Loaded(TelegramMediaWebpageLoadedContent(url: url, displayUrl: displayUrl, hash: hash, type: type, websiteName: siteName, title: title, text: description, embedUrl: embedUrl, embedType: embedType, embedSize: embedSize, duration: webpageDuration, author: author, isMediaLargeByDefault: isMediaLargeByDefault, image: image, file: file, story: story, attributes: webpageAttributes, instantPage: instantPage)))
case .webPageEmpty:
return nil
}
@ -66,16 +69,42 @@ public class WebpagePreviewMessageAttribute: MessageAttribute, Equatable {
public let associatedPeerIds: [PeerId] = []
public let associatedMediaIds: [MediaId] = []
public init() {
public let leadingPreview: Bool
public let forceLargeMedia: Bool?
public let isManuallyAdded: Bool
public init(leadingPreview: Bool, forceLargeMedia: Bool?, isManuallyAdded: Bool) {
self.leadingPreview = leadingPreview
self.forceLargeMedia = forceLargeMedia
self.isManuallyAdded = isManuallyAdded
}
required public init(decoder: PostboxDecoder) {
self.leadingPreview = decoder.decodeBoolForKey("lp", orElse: false)
self.forceLargeMedia = decoder.decodeOptionalBoolForKey("lm")
self.isManuallyAdded = decoder.decodeBoolForKey("ma", orElse: false)
}
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeBool(self.leadingPreview, forKey: "lp")
if let forceLargeMedia = self.forceLargeMedia {
encoder.encodeBool(forceLargeMedia, forKey: "lm")
} else {
encoder.encodeNil(forKey: "lm")
}
encoder.encodeBool(self.isManuallyAdded, forKey: "ma")
}
public static func ==(lhs: WebpagePreviewMessageAttribute, rhs: WebpagePreviewMessageAttribute) -> Bool {
if lhs.leadingPreview != rhs.leadingPreview {
return false
}
if lhs.forceLargeMedia != rhs.forceLargeMedia {
return false
}
if lhs.isManuallyAdded != rhs.isManuallyAdded {
return false
}
return true
}
}

View file

@ -10,12 +10,64 @@ public enum EnqueueMessageGrouping {
}
public struct EngineMessageReplyQuote: Codable, Equatable {
private enum CodingKeys: String, CodingKey {
case text = "t"
case entities = "e"
case media = "m"
}
public var text: String
public var entities: [MessageTextEntity]
public var media: Media?
public init(text: String, entities: [MessageTextEntity]) {
public init(text: String, entities: [MessageTextEntity], media: Media?) {
self.text = text
self.entities = entities
self.media = media
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.text = try container.decode(String.self, forKey: .text)
self.entities = try container.decode([MessageTextEntity].self, forKey: .entities)
if let mediaData = try container.decodeIfPresent(Data.self, forKey: .media) {
self.media = PostboxDecoder(buffer: MemoryBuffer(data: mediaData)).decodeRootObject() as? Media
} else {
self.media = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.text, forKey: .text)
try container.encode(self.entities, forKey: .entities)
if let media = self.media {
let mediaEncoder = PostboxEncoder()
mediaEncoder.encodeRootObject(media)
try container.encode(mediaEncoder.makeData(), forKey: .media)
}
}
public static func ==(lhs: EngineMessageReplyQuote, rhs: EngineMessageReplyQuote) -> Bool {
if lhs.text != rhs.text {
return false
}
if lhs.entities != rhs.entities {
return false
}
if let lhsMedia = lhs.media, let rhsMedia = rhs.media {
if !lhsMedia.isEqual(to: rhsMedia) {
return false
}
} else {
if (lhs.media == nil) != (rhs.media == nil) {
return false
}
}
return true
}
}
@ -166,6 +218,8 @@ private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAtt
return true
case _ as MediaSpoilerMessageAttribute:
return true
case _ as WebpagePreviewMessageAttribute:
return true
default:
return false
}
@ -510,7 +564,16 @@ func enqueueMessages(transaction: Transaction, account: Account, peerId: PeerId,
threadMessageId = replyMessage.effectiveReplyThreadMessageId
if quote == nil, replyToMessageId.messageId.peerId != peerId {
let nsText = replyMessage.text as NSString
quote = EngineMessageReplyQuote(text: replyMessage.text, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true))
var replyMedia: Media?
for m in replyMessage.media {
switch m {
case _ as TelegramMediaImage, _ as TelegramMediaFile:
replyMedia = m
default:
break
}
}
quote = EngineMessageReplyQuote(text: replyMessage.text, entities: messageTextEntitiesInRange(entities: replyMessage.textEntitiesAttribute?.entities ?? [], range: NSRange(location: 0, length: nsText.length), onlyQuoteable: true), media: replyMedia)
}
}
attributes.append(ReplyMessageAttribute(messageId: replyToMessageId.messageId, threadMessageId: threadMessageId, quote: quote))

View file

@ -95,6 +95,21 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(peer: inputPeer, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil))
}
|> castError(PendingMessageUploadError.self), .text)
} else if let media = media.first as? TelegramMediaWebpage, case let .Loaded(content) = media.content {
return .signal(postbox.transaction { transaction -> PendingMessageUploadedContentResult in
var flags: Int32 = 0
if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute {
if let forceLargeMedia = attribute.forceLargeMedia {
if forceLargeMedia {
flags |= 1 << 0
} else {
flags |= 1 << 1
}
}
}
return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil))
}
|> castError(PendingMessageUploadError.self), .text)
} else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) {
return .signal(mediaResult, .media)
} else {
@ -222,6 +237,18 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post
} else if let media = media as? TelegramMediaDice {
let inputDice = Api.InputMedia.inputMediaDice(emoticon: media.emoji)
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(inputDice, text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else if let media = media as? TelegramMediaWebpage, case let .Loaded(content) = media.content {
var flags: Int32 = 0
if let attribute = attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute {
if let forceLargeMedia = attribute.forceLargeMedia {
if forceLargeMedia {
flags |= 1 << 0
} else {
flags |= 1 << 1
}
}
}
return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaWebPage(flags: flags, url: content.url), text), reuploadInfo: nil, cacheReferenceKey: nil)))
} else {
return nil
}

View file

@ -64,7 +64,7 @@ private final class PendingUpdateMessageManagerImpl {
}
}
func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool) {
func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool) {
if let context = self.contexts[messageId] {
self.contexts.removeValue(forKey: messageId)
context.disposable.dispose()
@ -75,7 +75,7 @@ private final class PendingUpdateMessageManagerImpl {
self.contexts[messageId] = context
let queue = self.queue
disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: nil)
disposable.set((requestEditMessage(accountPeerId: self.stateManager.accountPeerId, postbox: self.postbox, network: self.network, stateManager: self.stateManager, transformOutgoingMessageMedia: self.transformOutgoingMessageMedia, messageMediaPreuploadManager: self.messageMediaPreuploadManager, mediaReferenceRevalidationContext: self.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: nil)
|> deliverOn(self.queue)).start(next: { [weak self, weak context] value in
queue.async {
guard let strongSelf = self, let initialContext = context else {
@ -163,9 +163,9 @@ public final class PendingUpdateMessageManager {
})
}
public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false) {
public func add(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false) {
self.impl.with { impl in
impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview)
impl.add(messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}

View file

@ -27,15 +27,15 @@ public enum RequestEditMessageError {
case invalidGrouping
}
func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime)
func _internal_requestEditMessage(account: Account, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessage(accountPeerId: account.peerId, postbox: account.postbox, network: account.network, stateManager: account.stateManager, transformOutgoingMessageMedia: account.transformOutgoingMessageMedia, messageMediaPreuploadManager: account.messageMediaPreuploadManager, mediaReferenceRevalidationContext: account.mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime)
}
func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false)
func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: false)
|> `catch` { error -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> in
if case .invalidReference = error {
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true)
return requestEditMessageInternal(accountPeerId: accountPeerId, postbox: postbox, network: network, stateManager: stateManager, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, mediaReferenceRevalidationContext: mediaReferenceRevalidationContext, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime, forceReupload: true)
} else {
return .fail(error)
}
@ -50,7 +50,7 @@ func requestEditMessage(accountPeerId: PeerId, postbox: Postbox, network: Networ
}
}
private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, network: Network, stateManager: AccountStateManager, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, mediaReferenceRevalidationContext: MediaReferenceRevalidationContext, messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute?, disableUrlPreview: Bool, scheduleTime: Int32?, forceReupload: Bool) -> Signal<RequestEditMessageResult, RequestEditMessageInternalError> {
let uploadedMedia: Signal<PendingMessageUploadedContentResult?, NoError>
switch media {
case .keep:
@ -59,7 +59,11 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
case let .update(media):
let generateUploadSignal: (Bool) -> Signal<PendingMessageUploadedContentResult, PendingMessageUploadError>? = { forceReupload in
let augmentedMedia = augmentMediaWithReference(media)
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: [])
var attributes: [MessageAttribute] = []
if let webpagePreviewAttribute = webpagePreviewAttribute {
attributes.append(webpagePreviewAttribute)
}
return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: attributes)
}
if let uploadSignal = generateUploadSignal(forceReupload) {
uploadedMedia = .single(.progress(0.027))
@ -164,6 +168,12 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox,
flags |= Int32(1 << 15)
}
if let webpagePreviewAttribute = webpagePreviewAttribute {
if webpagePreviewAttribute.leadingPreview {
flags |= Int32(1 << 16)
}
}
return network.request(Api.functions.messages.editMessage(flags: flags, peer: inputPeer, id: messageId.id, message: text, media: inputMedia, replyMarkup: nil, entities: apiEntities, scheduleDate: effectiveScheduleTime))
|> map { result -> Api.Updates? in
return result

View file

@ -1148,9 +1148,15 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
let messageText = text
var medias: [Media] = []
let (mediaValue, expirationTimer, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let (mediaValue, expirationTimer, nonPremium, hasSpoiler, webpageAttributes) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let mediaValue = mediaValue {
medias.append(mediaValue)
if mediaValue is TelegramMediaWebpage {
if let webpageAttributes {
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: webpageAttributes.forceLargeMedia, isManuallyAdded: webpageAttributes.isManuallyAdded))
}
}
}
if let expirationTimer = expirationTimer {
attributes.append(AutoclearTimeoutMessageAttribute(timeout: expirationTimer, countdownBeginTime: nil))
@ -1532,15 +1538,17 @@ private func finalStateWithUpdatesAndServerTime(accountPeerId: PeerId, postbox:
var replySubject: EngineMessageReplySubject?
if let replyToMsgHeader = replyToMsgHeader {
switch replyToMsgHeader {
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, _, replyToTopId, quoteText, quoteEntities):
case let .messageReplyHeader(_, replyToMsgId, replyToPeerId, replyHeader, replyMedia, replyToTopId, quoteText, quoteEntities):
let _ = replyHeader
let _ = replyMedia
let _ = replyToTopId
var quote: EngineMessageReplyQuote?
if let quoteText = quoteText {
quote = EngineMessageReplyQuote(
text: quoteText,
entities: messageTextEntitiesFromApiEntities(quoteEntities ?? [])
entities: messageTextEntitiesFromApiEntities(quoteEntities ?? []),
media: textMediaAndExpirationTimerFromApiMedia(replyMedia, accountPeerId).media
)
}
@ -4492,7 +4500,7 @@ func replayFinalState(
}
updatedExtendedMedia = .preview(dimensions: dimensions, immediateThumbnailData: immediateThumbnailData, videoDuration: videoDuration)
case let .messageExtendedMedia(apiMedia):
let (media, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
let (media, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
if let media = media {
updatedExtendedMedia = .full(media: media)
} else {

View file

@ -157,7 +157,7 @@ func applyUpdateMessage(postbox: Postbox, stateManager: AccountStateManager, mes
text = updatedMessage.text
forwardInfo = updatedMessage.forwardInfo
} else if case let .updateShortSentMessage(_, _, _, _, _, apiMedia, entities, ttlPeriod) = result {
let (mediaValue, _, nonPremium, hasSpoiler) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
let (mediaValue, _, nonPremium, hasSpoiler, _) = textMediaAndExpirationTimerFromApiMedia(apiMedia, currentMessage.id.peerId)
if let mediaValue = mediaValue {
media = [mediaValue]
} else {

View file

@ -1260,6 +1260,12 @@ public final class PendingMessageManager {
}
}
if let attribute = message.webpagePreviewAttribute {
if attribute.leadingPreview {
flags |= 1 << 16
}
}
sendMessageRequest = network.requestWithAdditionalInfo(Api.functions.messages.sendMessage(flags: flags, peer: inputPeer, replyTo: replyTo, message: message.text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), info: .acknowledgement, tag: dependencyTag)
case let .media(inputMedia, text):
if bubbleUpEmojiOrStickersets {
@ -1312,6 +1318,12 @@ public final class PendingMessageManager {
replyTo = .inputReplyToStory(userId: inputUser, storyId: replyToStoryId.id)
}
}
if let attribute = message.webpagePreviewAttribute {
if attribute.leadingPreview {
flags |= 1 << 16
}
}
sendMessageRequest = network.request(Api.functions.messages.sendMedia(flags: flags, peer: inputPeer, replyTo: replyTo, media: inputMedia, message: text, randomId: uniqueId, replyMarkup: nil, entities: messageEntities, scheduleDate: scheduleTime, sendAs: sendAsInputPeer), tag: dependencyTag)
|> map(NetworkRequestResult.result)

View file

@ -56,6 +56,14 @@ public class QuotedReplyMessageAttribute: MessageAttribute {
return []
}
public var associatedPeerIds: [PeerId] {
if let peerId = self.peerId {
return [peerId]
} else {
return []
}
}
public init(peerId: PeerId?, authorName: String?, quote: EngineMessageReplyQuote?) {
self.peerId = peerId
self.authorName = authorName

View file

@ -69,34 +69,6 @@ public final class TelegraMediaWebpageThemeAttribute: PostboxCoding, Equatable {
}
}
public struct TelegramMediaWebpageDisplayOptions: Codable, Equatable {
public enum CodingKeys: String, CodingKey {
case position = "p"
case largeMedia = "lm"
}
public enum Position: Int32, Codable {
case aboveText = 0
case belowText = 1
}
public var position: Position?
public var largeMedia: Bool?
public static let `default` = TelegramMediaWebpageDisplayOptions(
position: nil,
largeMedia: nil
)
public init(
position: Position?,
largeMedia: Bool?
) {
self.position = position
self.largeMedia = largeMedia
}
}
public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let url: String
public let displayUrl: String
@ -110,6 +82,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let embedSize: PixelDimensions?
public let duration: Int?
public let author: String?
public let isMediaLargeByDefault: Bool?
public let image: TelegramMediaImage?
public let file: TelegramMediaFile?
@ -117,8 +90,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
public let attributes: [TelegramMediaWebpageAttribute]
public let instantPage: InstantPage?
public let displayOptions: TelegramMediaWebpageDisplayOptions
public init(
url: String,
displayUrl: String,
@ -132,12 +103,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
embedSize: PixelDimensions?,
duration: Int?,
author: String?,
isMediaLargeByDefault: Bool?,
image: TelegramMediaImage?,
file: TelegramMediaFile?,
story: TelegramMediaStory?,
attributes: [TelegramMediaWebpageAttribute],
instantPage: InstantPage?,
displayOptions: TelegramMediaWebpageDisplayOptions
instantPage: InstantPage?
) {
self.url = url
self.displayUrl = displayUrl
@ -151,12 +122,12 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
self.embedSize = embedSize
self.duration = duration
self.author = author
self.isMediaLargeByDefault = isMediaLargeByDefault
self.image = image
self.file = file
self.story = story
self.attributes = attributes
self.instantPage = instantPage
self.displayOptions = displayOptions
}
public init(decoder: PostboxDecoder) {
@ -180,6 +151,7 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
self.duration = nil
}
self.author = decoder.decodeOptionalStringForKey("au")
self.isMediaLargeByDefault = decoder.decodeOptionalBoolForKey("lbd")
if let image = decoder.decodeObjectForKey("im") as? TelegramMediaImage {
self.image = image
@ -213,8 +185,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
self.instantPage = nil
}
self.displayOptions = decoder.decodeCodable(TelegramMediaWebpageDisplayOptions.self, forKey: "do") ?? TelegramMediaWebpageDisplayOptions.default
}
public func encode(_ encoder: PostboxEncoder) {
@ -268,6 +238,11 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "au")
}
if let isMediaLargeByDefault = self.isMediaLargeByDefault {
encoder.encodeBool(isMediaLargeByDefault, forKey: "lbd")
} else {
encoder.encodeNil(forKey: "lbd")
}
if let image = self.image {
encoder.encodeObject(image, forKey: "im")
} else {
@ -291,31 +266,6 @@ public final class TelegramMediaWebpageLoadedContent: PostboxCoding, Equatable {
} else {
encoder.encodeNil(forKey: "ip")
}
encoder.encodeCodable(self.displayOptions, forKey: "do")
}
public func withDisplayOptions(_ displayOptions: TelegramMediaWebpageDisplayOptions) -> TelegramMediaWebpageLoadedContent {
return TelegramMediaWebpageLoadedContent(
url: self.url,
displayUrl: self.displayUrl,
hash: self.hash,
type: self.type,
websiteName: self.websiteName,
title: self.title,
text: self.text,
embedUrl: self.embedUrl,
embedType: self.embedType,
embedSize: self.embedSize,
duration: self.duration,
author: self.author,
image: self.image,
file: self.file,
story: self.story,
attributes: self.attributes,
instantPage: self.instantPage,
displayOptions: displayOptions
)
}
}
@ -335,6 +285,10 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage
return false
}
if lhs.isMediaLargeByDefault != rhs.isMediaLargeByDefault {
return false
}
if let lhsImage = lhs.image, let rhsImage = rhs.image {
if !lhsImage.isEqual(to: rhsImage) {
return false
@ -373,10 +327,6 @@ public func ==(lhs: TelegramMediaWebpageLoadedContent, rhs: TelegramMediaWebpage
return false
}
if lhs.displayOptions != rhs.displayOptions {
return false
}
return true
}

View file

@ -330,7 +330,9 @@ public func messageTextEntitiesInRange(entities: [MessageTextEntity], range: NSR
}
}
if entity.range.overlaps(range) {
result.append(MessageTextEntity(range: entity.range.clamped(to: range), type: entity.type))
var mappedRange = entity.range.clamped(to: range)
mappedRange = (entity.range.lowerBound - range.lowerBound) ..< (entity.range.upperBound - range.lowerBound)
result.append(MessageTextEntity(range: mappedRange, type: entity.type))
}
}
return result

View file

@ -1052,7 +1052,7 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
}
id = idValue
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId)
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, toPeerId)
if let parsedMedia = parsedMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: postbox, force: originalMedia is TelegramMediaFile && parsedMedia is TelegramMediaFile)
}
@ -1170,7 +1170,7 @@ func _internal_editStory(account: Account, peerId: PeerId, id: Int32, media: Eng
if case let .updateStory(_, story) = update {
switch story {
case let .storyItem(_, _, _, _, _, _, media, _, _, _, _):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, account.peerId)
if let parsedMedia = parsedMedia, let originalMedia = originalMedia {
applyMediaResourceChanges(from: originalMedia, to: parsedMedia, postbox: account.postbox, force: false)
}
@ -1540,7 +1540,7 @@ extension Stories.StoredItem {
init?(apiStoryItem: Api.StoryItem, existingItem: Stories.Item? = nil, peerId: PeerId, transaction: Transaction) {
switch apiStoryItem {
case let .storyItem(flags, id, date, expireDate, caption, entities, media, mediaAreas, privacy, views, sentReaction):
let (parsedMedia, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
let (parsedMedia, _, _, _, _) = textMediaAndExpirationTimerFromApiMedia(media, peerId)
if let parsedMedia = parsedMedia {
var parsedPrivacy: Stories.Item.Privacy?
if let privacy = privacy {

View file

@ -118,8 +118,8 @@ public extension TelegramEngine {
return _internal_clearAuthorHistory(account: self.account, peerId: peerId, memberId: memberId)
}
public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime)
public func requestEditMessage(messageId: MessageId, text: String, media: RequestEditMessageMedia, entities: TextEntitiesMessageAttribute?, inlineStickers: [MediaId: Media], webpagePreviewAttribute: WebpagePreviewMessageAttribute? = nil, disableUrlPreview: Bool = false, scheduleTime: Int32? = nil) -> Signal<RequestEditMessageResult, RequestEditMessageError> {
return _internal_requestEditMessage(account: self.account, messageId: messageId, text: text, media: media, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview, scheduleTime: scheduleTime)
}
public func requestEditLiveLocation(messageId: MessageId, stop: Bool, coordinate: (latitude: Double, longitude: Double, accuracyRadius: Int32?)?, heading: Int32?, proximityNotificationRadius: Int32?) -> Signal<Void, NoError> {

View file

@ -437,6 +437,17 @@ public extension Message {
}
}
public extension Message {
var webpagePreviewAttribute: WebpagePreviewMessageAttribute? {
for attribute in self.attributes {
if let attribute = attribute as? WebpagePreviewMessageAttribute {
return attribute
}
}
return nil
}
}
public func _internal_parseMediaAttachment(data: Data) -> Media? {
guard let object = Api.parse(Buffer(buffer: MemoryBuffer(data: data))) else {
return nil

View file

@ -93,7 +93,26 @@ public func actualizedWebpage(account: Account, webpage: TelegramMediaWebpage) -
if let updatedWebpage = telegramMediaWebpageFromApiWebpage(apiWebpage, url: nil), case .Loaded = updatedWebpage.content, updatedWebpage.webpageId == webpage.webpageId {
return .single(updatedWebpage)
} else if case let .webPageNotModified(_, viewsValue) = apiWebpage, let views = viewsValue, case let .Loaded(content) = webpage.content {
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(url: content.url, displayUrl: content.displayUrl, hash: content.hash, type: content.type, websiteName: content.websiteName, title: content.title, text: content.text, embedUrl: content.embedUrl, embedType: content.embedType, embedSize: content.embedSize, duration: content.duration, author: content.author, image: content.image, file: content.file, story: content.story, attributes: content.attributes, instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) }), displayOptions: .default))
let updatedContent: TelegramMediaWebpageContent = .Loaded(TelegramMediaWebpageLoadedContent(
url: content.url,
displayUrl: content.displayUrl,
hash: content.hash,
type: content.type,
websiteName: content.websiteName,
title: content.title,
text: content.text,
embedUrl: content.embedUrl,
embedType: content.embedType,
embedSize: content.embedSize,
duration: content.duration,
author: content.author,
isMediaLargeByDefault: content.isMediaLargeByDefault,
image: content.image,
file: content.file,
story: content.story,
attributes: content.attributes,
instantPage: content.instantPage.flatMap({ InstantPage(blocks: $0.blocks, media: $0.media, isComplete: $0.isComplete, rtl: $0.rtl, url: $0.url, views: views) })
))
let updatedWebpage = TelegramMediaWebpage(webpageId: webpage.webpageId, content: updatedContent)
updateMessageMedia(transaction: transaction, id: webpage.webpageId, media: updatedWebpage)
return .single(updatedWebpage)

View file

@ -1287,9 +1287,10 @@ public struct PresentationResourcesChat {
public static func chatReplyBackgroundTemplateImage(_ theme: PresentationTheme) -> UIImage? {
return theme.image(PresentationResourceKey.chatReplyBackgroundTemplateImage.rawValue, { theme in
let radius: CGFloat = 3.0
let radius: CGFloat = 4.0
let lineWidth: CGFloat = 3.0
return generateImage(CGSize(width: radius * 2.0 + 1.0, height: radius * 2.0), rotatedContext: { size, context in
return generateImage(CGSize(width: radius * 2.0 + 4.0, height: radius * 2.0 + 8.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: radius).cgPath)
@ -1299,8 +1300,8 @@ public struct PresentationResourcesChat {
context.fill(CGRect(origin: CGPoint(), size: size))
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: radius, height: size.height)))
})?.stretchableImage(withLeftCapWidth: Int(radius), topCapHeight: Int(radius)).withRenderingMode(.alwaysTemplate)
context.fill(CGRect(origin: CGPoint(), size: CGSize(width: lineWidth, height: size.height)))
})?.stretchableImage(withLeftCapWidth: Int(radius) + 2, topCapHeight: Int(radius) + 3).withRenderingMode(.alwaysTemplate)
})
}

View file

@ -356,6 +356,12 @@ public func mediaContentKind(_ media: EngineMedia, message: EngineMessage? = nil
return .story
case .giveaway:
return .giveaway
case let .webpage(webpage):
if let message, message.text.isEmpty, case let .Loaded(content) = webpage.content {
return .text(NSAttributedString(string: content.displayUrl))
} else {
return nil
}
default:
return nil
}

View file

@ -427,7 +427,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed)
return .url(url: url, concealed: concealed, activate: nil)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
@ -454,8 +454,8 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
switch tapAction {
case .none, .ignore:
break
case let .url(url, concealed):
self.item?.controllerInteraction.openUrl(url, concealed, nil, nil)
case let .url(url, concealed, activate):
self.item?.controllerInteraction.openUrl(url, concealed, nil, nil, activate?())
case let .peerMention(peerId, _, _):
if let item = self.item {
let _ = (item.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
@ -480,7 +480,7 @@ public final class ChatBotInfoItemNode: ListViewItemNode {
switch tapAction {
case .none, .ignore:
break
case let .url(url, _):
case let .url(url, _, _):
item.controllerInteraction.longTap(.url(url), nil)
case let .peerMention(peerId, mention, _):
item.controllerInteraction.longTap(.peerMention(peerId, mention), nil)

View file

@ -378,7 +378,7 @@ public final class ChatButtonKeyboardInputNode: ChatInputNode {
self.controllerInteraction.sendMessage(markupButton.title)
dismissIfOnce = true
case let .url(url):
self.controllerInteraction.openUrl(url, true, nil, nil)
self.controllerInteraction.openUrl(url, true, nil, nil, nil)
case .requestMap:
self.controllerInteraction.shareCurrentLocation()
case .requestPhone:

View file

@ -47,6 +47,8 @@
return false;
}
return false;
return [super canPerformAction:action withSender:sender];
}

View file

@ -517,7 +517,7 @@ public class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.labelNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed)
return .url(url: url, concealed: concealed, activate: nil)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: true)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {

View file

@ -1967,7 +1967,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
})
} else if let attribute = attribute as? ReplyStoryAttribute {
return .optionalAction({
@ -1990,7 +1990,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
} else if let _ = forwardInfo.authorSignature {
@ -2269,7 +2269,7 @@ public class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
} else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
break
}
}

View file

@ -7,18 +7,16 @@ import ChatPresentationInterfaceState
import ShimmerEffect
private let buttonFont = Font.semibold(14.0)
private let sharedBackgroundImage = generateStretchableFilledCircleImage(radius: 4.0, color: UIColor.white)?.withRenderingMode(.alwaysTemplate)
public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
private let textNode: TextNode
private let iconNode: ASImageNode
private let highlightedTextNode: TextNode
private let backgroundNode: ASImageNode
private var iconView: UIImageView?
private let shimmerEffectNode: ShimmerEffectForegroundNode
private var regularImage: UIImage?
private var highlightedImage: UIImage?
private var backgroundView: UIImageView?
private var regularIconImage: UIImage?
private var highlightedIconImage: UIImage?
public var pressed: (() -> Void)?
@ -27,56 +25,24 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
public init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.highlightedTextNode = TextNode()
self.highlightedTextNode.isUserInteractionEnabled = false
self.shimmerEffectNode = ShimmerEffectForegroundNode()
self.shimmerEffectNode.cornerRadius = 5.0
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.shimmerEffectNode)
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.highlightedTextNode)
self.highlightedTextNode.isHidden = true
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.image = strongSelf.highlightedImage
strongSelf.iconNode.image = strongSelf.highlightedIconImage
strongSelf.textNode.isHidden = true
strongSelf.highlightedTextNode.isHidden = false
let scale = (strongSelf.bounds.width - 10.0) / strongSelf.bounds.width
strongSelf.layer.animateScale(from: 1.0, to: scale, duration: 0.15, removeOnCompletion: false)
} else {
if let presentationLayer = strongSelf.layer.presentation() {
strongSelf.layer.animateScale(from: CGFloat((presentationLayer.value(forKeyPath: "transform.scale.y") as? NSNumber)?.floatValue ?? 1.0), to: 1.0, duration: 0.25, removeOnCompletion: false)
}
if let snapshot = strongSelf.view.snapshotView(afterScreenUpdates: false) {
strongSelf.view.addSubview(snapshot)
snapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshot.removeFromSuperview()
})
}
strongSelf.backgroundNode.image = strongSelf.regularImage
strongSelf.iconNode.image = strongSelf.regularIconImage
strongSelf.textNode.isHidden = false
strongSelf.highlightedTextNode.isHidden = true
}
}
}
@ -95,7 +61,7 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
self.shimmerEffectNode.isHidden = false
self.shimmerEffectNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
let backgroundFrame = self.backgroundNode.frame
let backgroundFrame = self.bounds
self.shimmerEffectNode.frame = backgroundFrame
self.shimmerEffectNode.updateAbsoluteRect(CGRect(origin: .zero, size: backgroundFrame.size), within: backgroundFrame.size)
self.shimmerEffectNode.update(backgroundColor: .clear, foregroundColor: titleColor.withAlphaComponent(0.3), horizontal: true, effectSize: nil, globalTimeOffset: false, duration: nil)
@ -107,16 +73,13 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
})
}
public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage?, _ highlightedImage: UIImage?, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor, _ inProgress: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
let previousRegularImage = current?.regularImage
let previousHighlightedImage = current?.highlightedImage
public typealias AsyncLayout = (_ width: CGFloat, _ iconImage: UIImage?, _ cornerIcon: Bool, _ title: String, _ titleColor: UIColor, _ inProgress: Bool, _ drawBackground: Bool) -> (CGFloat, (CGFloat, CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))
public static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> AsyncLayout {
let previousRegularIconImage = current?.regularIconImage
let previousHighlightedIconImage = current?.highlightedIconImage
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout)
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, cornerIcon, title, titleColor, highlightedTitleColor, inProgress in
return { width, iconImage, cornerIcon, title, titleColor, inProgress, drawBackground in
let targetNode: ChatMessageAttachedContentButtonNode
if let current = current {
targetNode = current
@ -131,33 +94,11 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
makeTextLayout = TextNode.asyncLayout(targetNode.textNode)
}
let makeHighlightedTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout {
makeHighlightedTextLayout = maybeMakeHighlightedTextLayout
} else {
makeHighlightedTextLayout = TextNode.asyncLayout(targetNode.highlightedTextNode)
}
var updatedRegularImage: UIImage?
if regularImage !== previousRegularImage {
updatedRegularImage = regularImage
}
var updatedHighlightedImage: UIImage?
if highlightedImage !== previousHighlightedImage {
updatedHighlightedImage = highlightedImage
}
var updatedRegularIconImage: UIImage?
if iconImage !== previousRegularIconImage {
updatedRegularIconImage = iconImage
}
var updatedHighlightedIconImage: UIImage?
if highlightedIconImage !== previousHighlightedIconImage {
updatedHighlightedIconImage = highlightedIconImage
}
var iconWidth: CGFloat = 0.0
if let iconImage = iconImage {
iconWidth = iconImage.size.width + 5.0
@ -167,65 +108,70 @@ public final class ChatMessageAttachedContentButtonNode: HighlightTrackingButton
let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
let (_, highlightedTextApply) = makeHighlightedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
return (textSize.size.width + labelInset * 2.0, { refinedWidth, refinedHeight in
let size = CGSize(width: refinedWidth, height: refinedHeight)
return (size, {
return (size, { animation in
targetNode.accessibilityLabel = title
//targetNode.borderColor = UIColor.red.cgColor
//targetNode.borderWidth = 1.0
targetNode.titleColor = titleColor
if let updatedRegularImage = updatedRegularImage {
targetNode.regularImage = updatedRegularImage
if !targetNode.textNode.isHidden {
targetNode.backgroundNode.image = updatedRegularImage
}
}
if let updatedHighlightedImage = updatedHighlightedImage {
targetNode.highlightedImage = updatedHighlightedImage
if targetNode.textNode.isHidden {
targetNode.backgroundNode.image = updatedHighlightedImage
}
let iconView: UIImageView
if let current = targetNode.iconView {
iconView = current
} else {
iconView = UIImageView()
targetNode.iconView = iconView
targetNode.view.addSubview(iconView)
}
iconView.tintColor = titleColor
if let updatedRegularIconImage = updatedRegularIconImage {
targetNode.regularIconImage = updatedRegularIconImage
if !targetNode.textNode.isHidden {
targetNode.iconNode.image = updatedRegularIconImage
}
}
if let updatedHighlightedIconImage = updatedHighlightedIconImage {
targetNode.highlightedIconImage = updatedHighlightedIconImage
if targetNode.iconNode.isHidden {
targetNode.iconNode.image = updatedHighlightedIconImage
iconView.image = updatedRegularIconImage.withRenderingMode(.alwaysTemplate)
}
}
let _ = textApply()
let _ = highlightedTextApply()
let backgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: size.height))
var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((backgroundFrame.height - textSize.size.height) / 2.0)), size: textSize.size)
targetNode.backgroundNode.frame = backgroundFrame
if let image = targetNode.iconNode.image {
if drawBackground {
textFrame.origin.y += 1.0
}
if let image = iconView.image {
let iconFrame: CGRect
if cornerIcon {
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size)
iconFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX - image.size.width - 5.0, y: 5.0), size: image.size)
} else {
textFrame.origin.x += floor(image.size.width / 2.0)
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size)
iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + floorToScreenPixels((textFrame.height - image.size.height) * 0.5)), size: image.size)
}
if targetNode.iconNode.supernode == nil {
targetNode.addSubnode(targetNode.iconNode)
}
} else if targetNode.iconNode.supernode != nil {
targetNode.iconNode.removeFromSupernode()
animation.animator.updateFrame(layer: iconView.layer, frame: iconFrame, completion: nil)
}
targetNode.textNode.frame = textFrame
targetNode.highlightedTextNode.frame = targetNode.textNode.frame
targetNode.textNode.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
animation.animator.updatePosition(layer: targetNode.textNode.layer, position: textFrame.center, completion: nil)
if drawBackground {
let backgroundView: UIImageView
if let current = targetNode.backgroundView {
backgroundView = current
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
} else {
backgroundView = UIImageView()
backgroundView.image = sharedBackgroundImage
targetNode.backgroundView = backgroundView
targetNode.view.insertSubview(backgroundView, at: 0)
backgroundView.frame = backgroundFrame
}
backgroundView.tintColor = titleColor.withMultipliedAlpha(0.1)
} else if let backgroundView = targetNode.backgroundView {
targetNode.backgroundView = nil
backgroundView.removeFromSuperview()
}
return targetNode
})

View file

@ -11,6 +11,7 @@ swift_library(
],
deps = [
"//submodules/AsyncDisplayKit",
"//submodules/SSignalKit/SwiftSignalKit",
"//submodules/Display",
"//submodules/Postbox",
"//submodules/TelegramCore",

View file

@ -11,6 +11,7 @@ import ChatMessageBackground
import ChatControllerInteraction
import ChatHistoryEntry
import ChatMessageItemCommon
import SwiftSignalKit
public enum ChatMessageBubbleContentBackgroundHiding {
case never
@ -73,7 +74,10 @@ public enum ChatMessageBubbleMergeStatus {
public enum ChatMessageBubbleRelativePosition {
public enum NeighbourType {
case media
case freeform
case header
case footer
case text
case reactions
}
public enum NeighbourSpacing {
@ -119,7 +123,7 @@ public enum ChatMessageBubblePreparePosition {
public enum ChatMessageBubbleContentTapAction {
case none
case url(url: String, concealed: Bool)
case url(url: String, concealed: Bool, activate: (() -> Promise<Bool>?)?)
case textMention(String)
case peerMention(peerId: PeerId, mention: String, openProfile: Bool)
case botCommand(String)

View file

@ -105,7 +105,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
outer: for (message, itemAttributes) in item.content {
for attribute in message.attributes {
if let attribute = attribute as? RestrictedContentMessageAttribute, attribute.platformText(platform: "ios", contentSettings: item.context.currentContentSettings.with { $0 }) != nil {
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
break outer
}
@ -121,9 +121,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} else if let story = media as? TelegramMediaStory {
if story.isMention {
if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
} else {
var hideStory = false
@ -154,7 +154,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if isVideo {
if file.isInstantVideo {
hasSeparateCommentsButton = true
result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageInstantVideoBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else {
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty {
messageWithCaptionToAdd = (message, itemAttributes)
@ -168,30 +168,30 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
}
isFile = true
hasFiles = true
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing)))
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: neighborSpacing)))
needReactions = false
}
} else if let action = media as? TelegramMediaAction {
isAction = true
if case .phoneCall = action.action {
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .giftPremium = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .suggestedProfilePhoto = action.action {
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageProfilePhotoSuggestionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .setChatWallpaper = action.action {
result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageWallpaperBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else if case .giftCode = action.action {
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageGiftBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
needReactions = false
} else if let _ = media as? TelegramMediaMap {
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else if let _ = media as? TelegramMediaGame {
skipText = true
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
break inner
} else if let invoice = media as? TelegramMediaInvoice {
@ -199,23 +199,23 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else {
skipText = true
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
needReactions = false
break inner
} else if let _ = media as? TelegramMediaContact {
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaExpiredContent {
result.removeAll()
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
return (result, false, false)
} else if let _ = media as? TelegramMediaPoll {
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaGiveaway {
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageGiveawayBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
} else if let _ = media as? TelegramMediaUnsupported {
isUnsupportedMedia = true
@ -235,7 +235,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
messageWithCaptionToAdd = (message, itemAttributes)
skipText = true
} else {
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: isFile ? .condensed : .default)))
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: isFile ? .condensed : .default)))
needReactions = false
}
} else {
@ -255,10 +255,10 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
}
}
if content.displayOptions.position == .aboveText {
result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)), at: 0)
if let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute, attribute.leadingPreview {
result.insert((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)), at: 0)
} else {
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
}
needReactions = false
}
@ -269,31 +269,31 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if message.adAttribute != nil {
result.removeAll()
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
}
if isUnsupportedMedia {
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
}
}
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
}
if let additionalContent = item.additionalContent {
switch additionalContent {
case let .eventLogPreviousMessage(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
case let .eventLogPreviousDescription(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
case let .eventLogPreviousLink(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)))
needReactions = false
}
}
@ -308,28 +308,26 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
if !isAction && !hasSeparateCommentsButton && !Namespaces.Message.allScheduled.contains(firstMessage.id.namespace) {
if hasCommentButton(item: item) {
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .footer, neighborSpacing: .default)))
}
}
if !reactionsAreInline, let reactionsAttribute = mergedMessageReactions(attributes: firstMessage.attributes), !reactionsAttribute.reactions.isEmpty {
if result.last?.1 == ChatMessageTextBubbleContentNode.self {
} else {
if result.last?.1 == ChatMessageWebpageBubbleContentNode.self ||
result.last?.1 == ChatMessagePollBubbleContentNode.self ||
if result.last?.1 == ChatMessagePollBubbleContentNode.self ||
result.last?.1 == ChatMessageContactBubbleContentNode.self ||
result.last?.1 == ChatMessageGameBubbleContentNode.self ||
result.last?.1 == ChatMessageInvoiceBubbleContentNode.self ||
result.last?.1 == ChatMessageGiveawayBubbleContentNode.self {
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
result.append((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default)))
needReactions = false
} else if result.last?.1 == ChatMessageCommentFooterContentNode.self {
if result.count >= 2 {
if result[result.count - 2].1 == ChatMessageWebpageBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self ||
if result[result.count - 2].1 == ChatMessagePollBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessageContactBubbleContentNode.self ||
result[result.count - 2].1 == ChatMessageGiveawayBubbleContentNode.self {
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)), at: result.count - 1)
result.insert((firstMessage, ChatMessageReactionsFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .reactions, neighborSpacing: .default)), at: result.count - 1)
}
}
}
@ -705,7 +703,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
if let singleUrl = accessibilityData.singleUrl {
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage)
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage, nil)
return true
}
@ -994,7 +992,18 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
recognizer.tapActionAtPoint = { [weak self] point in
if let strongSelf = self {
if let item = strongSelf.item, let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject {
if case .link = info {
if case let .link(link) = info {
let options = Atomic<ChatControllerSubject.LinkOptions?>(value: nil)
link.options.start(next: { value in
let _ = options.swap(value)
}).dispose()
guard let options = options.with({ $0 }) else {
return .fail
}
if !options.hasAlternativeLinks {
return .fail
}
for contentNode in strongSelf.contentNodes {
let contentNodePoint = strongSelf.view.convert(point, to: contentNode.view)
let tapAction = contentNode.tapActionAtPoint(contentNodePoint, gesture: .tap, isEstimating: true)
@ -1110,6 +1119,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
recognizer.highlight = { [weak self] point in
if let strongSelf = self {
if let replyInfoNode = strongSelf.replyInfoNode {
var translatedPoint: CGPoint?
let convertedNodeFrame = replyInfoNode.view.convert(replyInfoNode.bounds, to: strongSelf.view)
if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
translatedPoint = strongSelf.view.convert(point, to: replyInfoNode.view)
}
replyInfoNode.updateTouchesAtPoint(translatedPoint)
}
for contentNode in strongSelf.contentNodes {
var translatedPoint: CGPoint?
let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view)
@ -1240,7 +1257,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: isSelected)
let fontSize = floor(item.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let nameFont = Font.medium(fontSize)
let nameFont = Font.semibold(fontSize)
let inlineBotPrefixFont = Font.regular(fontSize)
@ -1709,8 +1726,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)
if index != 0 {
topBubbleAttributes = contentPropertiesAndPrepareLayouts[index - 1].3
}
@ -1903,7 +1920,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
if displayHeader {
firstNodeTopPosition = .Neighbour(false, .freeform, .default)
firstNodeTopPosition = .Neighbour(false, .header, .default)
} else {
firstNodeTopPosition = .None(topNodeMergeStatus)
}
@ -2027,7 +2044,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let bubbleWidthInsets: CGFloat = mosaicRange == nil ? layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right : 0.0
if authorNameString != nil || inlineBotNameString != nil {
if headerSize.height.isZero {
headerSize.height += 5.0
headerSize.height += 7.0
}
let inlineBotNameColor = messageTheme.accentTextColor
@ -2194,9 +2211,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if !isInstantVideo, hasReply, (replyMessage != nil || replyForward != nil || replyStory != nil) {
if headerSize.height.isZero {
headerSize.height += 10.0
headerSize.height += 11.0
} else {
headerSize.height += 1.0
headerSize.height += 2.0
}
let sizeAndApply = replyInfoLayout(ChatMessageReplyInfoNode.Arguments(
presentationData: item.presentationData,
@ -2218,10 +2235,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
replyInfoOriginY = headerSize.height
headerSize.width = max(headerSize.width, replyInfoSizeApply.0.width + bubbleWidthInsets)
headerSize.height += replyInfoSizeApply.0.height + 7.0
}
if !headerSize.height.isZero {
headerSize.height -= 5.0
if !headerSize.height.isZero {
headerSize.height -= 7.0
}
} else {
if !headerSize.height.isZero {
headerSize.height -= 5.0
}
}
}
@ -2353,7 +2374,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if mosaicRange.upperBound - 1 == contentNodeCount - 1 {
lastMosaicBottomPosition = lastNodeTopPosition
} else {
lastMosaicBottomPosition = .Neighbour(false, .freeform, .default)
lastMosaicBottomPosition = .Neighbour(false, .text, .default)
}
if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition {
@ -2421,8 +2442,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .text, neighborSpacing: .default)
if i != 0 {
topBubbleAttributes = contentPropertiesAndLayouts[i - 1].3
}
@ -2445,7 +2466,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contentPosition = .linear(top: topPosition, bottom: bottomPosition)
case .mosaic:
assertionFailure()
contentPosition = .linear(top: .Neighbour(false, .freeform, .default), bottom: .Neighbour(false, .freeform, .default))
contentPosition = .linear(top: .Neighbour(false, .text, .default), bottom: .Neighbour(false, .text, .default))
}
let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition)
#if DEBUG
@ -3267,6 +3288,12 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
contextSourceNode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode ?? strongSelf.mainContextSourceNode
containerSupernode = strongSelf.contentContainers.first(where: { $0.contentMessageStableId == contentNodeMessage.stableId })?.sourceNode.contentNode ?? strongSelf.clippingNode
}
#if DEBUG && false
contentNode.layer.borderColor = UIColor(white: 0.0, alpha: 0.2).cgColor
contentNode.layer.borderWidth = 1.0
#endif
containerSupernode.addSubnode(contentNode)
contentNode.itemNode = strongSelf
@ -3855,11 +3882,11 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
return .action({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
})
} else if let attribute = attribute as? ReplyStoryAttribute {
return .optionalAction({
return .action({
item.controllerInteraction.navigateToStory(item.message, attribute.storyId)
})
}
@ -3888,7 +3915,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
} else if let _ = forwardInfo.authorSignature {
@ -3937,9 +3964,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
return .action({
})
}
case let .url(url, concealed):
case let .url(url, concealed, activate):
return .action({
self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage)
self.item?.controllerInteraction.openUrl(url, concealed, nil, self.item?.content.firstMessage, activate?())
})
case let .peerMention(peerId, _, openProfile):
return .action({ [weak self] in
@ -4052,6 +4079,21 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
if let threadInfoNode = self.threadInfoNode, self.item?.controllerInteraction.tapMessage == nil, threadInfoNode.frame.contains(location) {
return .action({})
}
if let replyInfoNode = self.replyInfoNode, self.item?.controllerInteraction.tapMessage == nil, replyInfoNode.frame.contains(location) {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
return .action({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
})
} else if let attribute = attribute as? ReplyStoryAttribute {
return .action({
item.controllerInteraction.navigateToStory(item.message, attribute.storyId)
})
}
}
}
}
var tapMessage: Message? = item.content.firstMessage
var selectAll = true
@ -4081,7 +4123,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
switch tapAction {
case .none, .ignore:
break
case let .url(url, _):
case let .url(url, _, _):
return .action({
item.controllerInteraction.longTap(.url(url), message)
})
@ -4436,6 +4478,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
}
var highlighted = false
var highlightedQuote: String?
for contentNode in self.contentNodes {
let _ = contentNode.updateHighlightedState(animated: animated)
@ -4445,6 +4488,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
for (message, _) in item.content {
if highlightedState.messageStableId == message.stableId {
highlighted = true
highlightedQuote = highlightedState.quote
break
}
}
@ -4459,6 +4503,20 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
self.backgroundNode.setType(type: backgroundType, highlighted: highlighted, graphics: graphics, maskMode: self.backgroundMaskMode, hasWallpaper: hasWallpaper, transition: animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate, backgroundNode: item.controllerInteraction.presentationContext.backgroundNode)
}
}
if let highlightedQuote {
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: highlightedQuote, animated: animated)
}
}
} else {
for contentNode in self.contentNodes {
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
contentNode.updateQuoteTextHighlightState(text: nil, animated: animated)
}
}
}
}
@objc private func shareButtonPressed() {
@ -4468,7 +4526,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
} else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
break
}
}

View file

@ -246,30 +246,17 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
))
}
let buttonImage: UIImage
let buttonHighlightedImage: UIImage
let titleColor: UIColor
let titleHighlightedColor: UIColor
let avatarPlaceholderColor: UIColor
if incoming {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor
} else {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
avatarPlaceholderColor = item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, titleHighlightedColor, false)
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, item.presentationData.strings.Conversation_ViewContactDetails, titleColor, false, true)
var maxContentWidth: CGFloat = avatarSize.width + 7.0
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
@ -321,7 +308,7 @@ public class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
let _ = titleApply()
let _ = textApply()
let _ = buttonApply()
let _ = buttonApply(animation)
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 1.0), size: titleLayout.size)
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: avatarFrame.maxX + 7.0, y: avatarFrame.minY + 20.0), size: textLayout.size)

View file

@ -193,7 +193,7 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
}
public enum LayoutInput {
case trailingContent(contentWidth: CGFloat, reactionSettings: TrailingReactionSettings?)
case trailingContent(contentWidth: CGFloat?, reactionSettings: TrailingReactionSettings?)
case standalone(reactionSettings: StandaloneReactionSettings?)
public var displayInlineReactions: Bool {
@ -848,14 +848,20 @@ public class ChatMessageDateAndStatusNode: ASDisplayNode {
if reactionButtonsSize.width.isZero {
verticalReactionsInset = 0.0
if contentWidth + layoutSize.width > arguments.constrainedSize.width {
if let contentWidth {
if contentWidth + layoutSize.width > arguments.constrainedSize.width {
resultingWidth = layoutSize.width
verticalInset = 0.0
resultingHeight = layoutSize.height + verticalInset
} else {
resultingWidth = contentWidth + layoutSize.width
verticalInset = -layoutSize.height
resultingHeight = 0.0
}
} else {
resultingWidth = layoutSize.width
verticalInset = 0.0
resultingHeight = layoutSize.height + verticalInset
} else {
resultingWidth = contentWidth + layoutSize.width
verticalInset = -layoutSize.height
resultingHeight = 0.0
}
} else {
var additionalVerticalInset: CGFloat = 0.0

View file

@ -513,7 +513,7 @@ public class ChatMessageGiftBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.labelNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed)
return .url(url: url, concealed: concealed, activate: nil)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {

View file

@ -380,27 +380,14 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
))
}
let buttonImage: UIImage
let buttonHighlightedImage: UIImage
let titleColor: UIColor
let titleHighlightedColor: UIColor
if incoming {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.incoming.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: true, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
} else {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(item.presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(item.presentationData.theme.theme)!
titleColor = item.presentationData.theme.theme.chat.message.outgoing.accentTextColor
let bubbleColors = bubbleColorComponents(theme: item.presentationData.theme.theme, incoming: false, wallpaper: !item.presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColors.fill[0]
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, "LEARN MORE", titleColor, titleHighlightedColor, false)
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, nil, false, "LEARN MORE", titleColor, false, true)
let months = giveaway?.months ?? 0
let animationName: String
@ -476,7 +463,7 @@ public class ChatMessageGiveawayBubbleContentNode: ChatMessageBubbleContentNode
let _ = dateTitleApply()
let _ = dateTextApply()
let _ = channelButtonApply()
let _ = buttonApply()
let _ = buttonApply(animation)
let smallSpacing: CGFloat = 2.0
let largeSpacing: CGFloat = 14.0

View file

@ -960,7 +960,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
})
}
}
@ -979,7 +979,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
return
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
} else if let _ = forwardInfo.authorSignature {
@ -1026,7 +1026,7 @@ public class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureReco
} else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
break
}
}

View file

@ -37,6 +37,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ChatMessageReplyInfoNode",
"//submodules/TelegramUI/Components/Chat/InstantVideoRadialStatusNode",
"//submodules/TelegramUI/Components/Chat/ChatInstantVideoMessageDurationNode",
"//submodules/TelegramUI/Components/ChatControllerInteraction",
],
visibility = [
"//visibility:public",

View file

@ -26,6 +26,7 @@ import ChatMessageBubbleContentNode
import ChatMessageReplyInfoNode
import InstantVideoRadialStatusNode
import ChatInstantVideoMessageDurationNode
import ChatControllerInteraction
public struct ChatMessageInstantVideoItemLayoutResult {
public let contentSize: CGSize
@ -1288,7 +1289,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
if let item = self.item {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
return
} else if let attribute = attribute as? ReplyStoryAttribute {
item.controllerInteraction.navigateToStory(item.message, attribute.storyId)
@ -1309,7 +1310,7 @@ public class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
return
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
return
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)

View file

@ -134,7 +134,7 @@ public struct ChatMessageItemLayoutConstants {
}
public static var compact: ChatMessageItemLayoutConstants {
let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.85), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 11.0, bottom: 6.0 - UIScreenPixel, right: 11.0))
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 0.0, maxDimensions: CGSize(width: 300.0, height: 380.0), minDimensions: CGSize(width: 170.0, height: 74.0))
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
@ -146,7 +146,7 @@ public struct ChatMessageItemLayoutConstants {
}
public static var regular: ChatMessageItemLayoutConstants {
let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 5.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
let bubble = ChatMessageItemBubbleLayoutConstants(edgeInset: 3.0, defaultSpacing: 2.0 + UIScreenPixel, mergedSpacing: 0.0, maximumWidthFill: ChatMessageItemWidthFill(compactInset: 36.0, compactWidthBoundary: 500.0, freeMaximumFillFactor: 0.65), minimumSize: CGSize(width: 40.0, height: 35.0), contentInsets: UIEdgeInsets(top: 0.0, left: 6.0, bottom: 0.0, right: 0.0), borderInset: UIScreenPixel, strokeInsets: UIEdgeInsets(top: 1.0, left: 1.0, bottom: 1.0, right: 1.0))
let text = ChatMessageItemTextLayoutConstants(bubbleInsets: UIEdgeInsets(top: 6.0 + UIScreenPixel, left: 10.0, bottom: 6.0 - UIScreenPixel, right: 10.0))
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)

View file

@ -504,7 +504,17 @@ public final class ChatMessageItemImpl: ChatMessageItem, CustomStringConvertible
async {
let (top, bottom, dateAtBottom) = self.mergedWithItems(top: previousItem, bottom: nextItem)
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !self.disableDate)
var disableDate = self.disableDate
if let subject = self.associatedData.subject, case let .messageOptions(_, _, info) = subject {
switch info {
case .reply, .link:
disableDate = true
default:
break
}
}
let (layout, apply) = nodeLayout(self, params, top, bottom, dateAtBottom && !disableDate)
Queue.mainQueue().async {
completion(layout, { info in
apply(animation, info, false)

View file

@ -753,7 +753,7 @@ open class ChatMessageItemView: ListViewItemNode, ChatMessageItemNodeProtocol {
if url.hasPrefix("tg://") {
concealed = false
}
item.controllerInteraction.openUrl(url, concealed, nil, nil)
item.controllerInteraction.openUrl(url, concealed, nil, nil, nil)
case .requestMap:
item.controllerInteraction.shareCurrentLocation()
case .requestPhone:

View file

@ -171,7 +171,7 @@ public class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
if activeLiveBroadcastingTimeout != nil || selectedMedia?.venue != nil {
var relativePosition = position
if case let .linear(top, _) = position {
relativePosition = .linear(top: top, bottom: .Neighbour(false, .freeform, .default))
relativePosition = .linear(top: top, bottom: .Neighbour(false, .text, .default))
}
imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData)

View file

@ -1608,7 +1608,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed)
return .url(url: url, concealed: concealed, activate: nil)
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {

View file

@ -131,11 +131,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
return { arguments in
let fontSize = floor(arguments.presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
let titleFont = Font.medium(fontSize)
let titleFont = Font.semibold(fontSize)
let textFont = Font.regular(fontSize)
var titleString: String
let textString: NSAttributedString
var textString: NSAttributedString
let isMedia: Bool
let isText: Bool
var isExpiredStory: Bool = false
@ -165,12 +165,29 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
isMedia = isMediaValue
isText = isTextValue
} else if let replyForward = arguments.replyForward {
titleString = replyForward.authorName ?? " "
if let replyAuthorId = replyForward.peerId, let replyAuthor = arguments.parentMessage.peers[replyAuthorId] {
titleString = EnginePeer(replyAuthor).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
} else {
titleString = replyForward.authorName ?? " "
}
//TODO:localize
textString = NSAttributedString(string: replyForward.quote?.text ?? "Message")
isMedia = false
isText = true
if let media = replyForward.quote?.media {
if let text = replyForward.quote?.text, !text.isEmpty {
} else {
if let contentKind = mediaContentKind(EngineMedia(media), message: nil, strings: arguments.strings, nameDisplayOrder: arguments.presentationData.nameDisplayOrder, dateTimeFormat: arguments.presentationData.dateTimeFormat, accountPeerId: arguments.context.account.peerId) {
let (string, _) = stringForMediaKind(contentKind, strings: arguments.strings)
textString = string
} else {
textString = NSAttributedString(string: "Message")
}
}
isMedia = true
} else {
isMedia = false
}
isText = replyForward.quote?.text != nil && replyForward.quote?.text != ""
} else if let story = arguments.story {
if let authorPeer = arguments.parentMessage.peers[story.peerId] {
titleString = EnginePeer(authorPeer).displayTitle(strings: arguments.strings, displayOrder: arguments.presentationData.nameDisplayOrder)
@ -310,7 +327,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
} else {
messageText = NSAttributedString(string: text, font: textFont, textColor: textColor)
}
} else if let replyForward = arguments.replyForward, let quote = replyForward.quote {
} else if isText, let replyForward = arguments.replyForward, let quote = replyForward.quote {
let entities = quote.entities.filter { entity in
if case .Strikethrough = entity.type {
return true
@ -376,6 +393,24 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
}
}
}
} else if let replyForward = arguments.replyForward, let media = replyForward.quote?.media {
if let image = media as? TelegramMediaImage {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: image)
if let representation = largestRepresentationForPhoto(image) {
imageDimensions = representation.dimensions.cgSize
}
} else if let file = media as? TelegramMediaFile, file.isVideo && !file.isVideoSticker {
updatedMediaReference = .message(message: MessageReference(arguments.parentMessage), media: file)
if let dimensions = file.dimensions {
imageDimensions = dimensions.cgSize
} else if let representation = largestImageRepresentation(file.previewRepresentations), !file.isSticker {
imageDimensions = representation.dimensions.cgSize
}
if file.isInstantVideo {
hasRoundImage = true
}
}
}
var imageTextInset: CGFloat = 0.0
@ -608,6 +643,10 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
node.backgroundView.tintColor = mainColor
node.backgroundView.frame = backgroundFrame
#if DEBUG && false
node.backgroundColor = .blue
#endif
if arguments.quote != nil || arguments.replyForward?.quote != nil {
let quoteIconView: UIImageView
if let current = node.quoteIconView {
@ -632,6 +671,17 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
})
}
}
public func updateTouchesAtPoint(_ point: CGPoint?) {
var isHighlighted = false
if point != nil {
isHighlighted = true
}
let transition: ContainedViewLayoutTransition = .animated(duration: isHighlighted ? 0.1 : 0.2, curve: .easeInOut)
let scale: CGFloat = isHighlighted ? ((self.bounds.width - 5.0) / self.bounds.width) : 1.0
transition.updateSublayerTransformScale(node: self, scale: scale)
}
public func animateFromInputPanel(sourceReplyPanel: TransitionReplyPanel, unclippedTransitionNode: ASDisplayNode? = nil, localRect: CGRect, transition: CombinedTransition) -> CGPoint {
let sourceParentNode = ASDisplayNode()

View file

@ -1334,7 +1334,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
for attribute in item.message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
return .optionalAction({
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text))
})
} else if let attribute = attribute as? ReplyStoryAttribute {
return .optionalAction({
@ -1357,7 +1357,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
return
}
}
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId)
item.controllerInteraction.navigateToMessage(item.message.id, sourceMessageId, NavigateToMessageParams(timestamp: nil, quote: nil))
} else if let peer = forwardInfo.source ?? forwardInfo.author {
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
} else if let _ = forwardInfo.authorSignature {
@ -1411,7 +1411,7 @@ public class ChatMessageStickerItemNode: ChatMessageItemView {
} else if item.content.firstMessage.id.peerId.isRepliesOrSavedMessages(accountPeerId: item.context.account.peerId) {
for attribute in item.content.firstMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId)
item.controllerInteraction.navigateToMessage(item.content.firstMessage.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
break
}
}

View file

@ -35,7 +35,7 @@ swift_library(
"//submodules/TelegramUI/Components/Chat/ShimmeringLinkNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
"//submodules/TelegramUI/Components/Chat/MessageQuoteComponent",
"//submodules/TelegramUI/Components/RichTextView",
"//submodules/TelegramUI/Components/TextLoadingEffect",
],
visibility = [
"//visibility:public",

View file

@ -24,7 +24,7 @@ import ChatMessageDateAndStatusNode
import ChatMessageBubbleContentNode
import ShimmeringLinkNode
import ChatMessageItemCommon
import RichTextView
import TextLoadingEffect
private final class CachedChatMessageText {
let text: String
@ -72,6 +72,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
private var linkPreviewOptionsDisposable: Disposable?
private var linkPreviewHighlightingNodes: [LinkHighlightingNode] = []
private var quoteHighlightingNodes: [LinkHighlightingNode] = []
private var linkProgressRange: NSRange?
private var linkProgressView: TextLoadingEffectView?
private var linkProgressDisposable: Disposable?
override public var visibility: ListViewItemNodeVisibility {
didSet {
if oldValue != self.visibility {
@ -107,7 +113,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.textAccessibilityOverlayNode)
self.textAccessibilityOverlayNode.openUrl = { [weak self] url in
self?.item?.controllerInteraction.openUrl(url, false, false, nil)
self?.item?.controllerInteraction.openUrl(url, false, false, nil, nil)
}
self.statusNode.reactionSelected = { [weak self] value in
@ -133,6 +139,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
deinit {
self.linkPreviewOptionsDisposable?.dispose()
self.linkProgressDisposable?.dispose()
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
@ -146,6 +153,31 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
var topInset: CGFloat = 0.0
var bottomInset: CGFloat = 0.0
if case let .linear(top, bottom) = position {
switch top {
case .None:
topInset = layoutConstants.text.bubbleInsets.top
case let .Neighbour(_, topType, _):
switch topType {
case .text:
topInset = layoutConstants.text.bubbleInsets.top - 2.0
case .header, .footer, .media, .reactions:
topInset = layoutConstants.text.bubbleInsets.top
}
default:
topInset = layoutConstants.text.bubbleInsets.top
}
switch bottom {
case .None:
bottomInset = layoutConstants.text.bubbleInsets.bottom
default:
bottomInset = layoutConstants.text.bubbleInsets.bottom - 3.0
}
}
let message = item.message
let incoming: Bool
@ -353,7 +385,12 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
let textFont = item.presentationData.messageFont
if let entities = entities {
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, message: item.message, adjustQuoteFontSize: true)
var underlineLinks = true
if !messageTheme.primaryTextColor.isEqual(messageTheme.linkTextColor) {
underlineLinks = false
}
attributedText = stringWithAppliedEntities(rawText, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseQuoteTintColor: messageTheme.accentTextColor, baseFont: textFont, linkFont: textFont, boldFont: item.presentationData.messageBoldFont, italicFont: item.presentationData.messageItalicFont, boldItalicFont: item.presentationData.messageBoldItalicFont, fixedFont: item.presentationData.messageFixedFont, blockQuoteFont: item.presentationData.messageBlockQuoteFont, underlineLinks: underlineLinks, message: item.message, adjustQuoteFontSize: true)
} else if !rawText.isEmpty {
attributedText = NSAttributedString(string: rawText, font: textFont, textColor: messageTheme.primaryTextColor)
} else {
@ -436,8 +473,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
var textFrame = CGRect(origin: CGPoint(x: -textInsets.left, y: -textInsets.top), 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))
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: layoutConstants.text.bubbleInsets.top)
textFrame = textFrame.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset)
textFrameWithoutInsets = textFrameWithoutInsets.offsetBy(dx: layoutConstants.text.bubbleInsets.left, dy: topInset)
var suggestedBoundingWidth: CGFloat = textFrameWithoutInsets.width
if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue {
@ -457,7 +494,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
boundingSize.width += layoutConstants.text.bubbleInsets.left + layoutConstants.text.bubbleInsets.right
boundingSize.height += layoutConstants.text.bubbleInsets.top + layoutConstants.text.bubbleInsets.bottom
boundingSize.height += topInset + bottomInset
return (boundingSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
@ -605,11 +643,15 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return
}
strongSelf.updateLinkPreviewTextHighlightState(text: options.url)
if options.hasAlternativeLinks {
strongSelf.updateLinkPreviewTextHighlightState(text: options.url)
}
})
}
}
}
strongSelf.updateLinkProgressState()
}
})
})
@ -646,10 +688,38 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
return .none
} else if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
var urlRange: NSRange?
if let (attributeText, fullText, urlRangeValue) = self.textNode.textNode.attributeSubstringWithRange(name: TelegramTextAttributes.URL, index: index) {
urlRange = urlRangeValue
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
}
return .url(url: url, concealed: concealed)
return .url(url: url, concealed: concealed, activate: { [weak self] in
guard let self else {
return nil
}
let promise = Promise<Bool>()
self.linkProgressDisposable?.dispose()
if self.linkProgressRange != nil {
self.linkProgressRange = nil
self.updateLinkProgressState()
}
self.linkProgressDisposable = (promise.get() |> deliverOnMainQueue).startStrict(next: { [weak self] value in
guard let self else {
return
}
let updatedRange: NSRange? = value ? urlRange : nil
if self.linkProgressRange != updatedRange {
self.linkProgressRange = updatedRange
self.updateLinkProgressState()
}
})
return promise
})
} else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false)
} else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
@ -811,6 +881,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
guard let item = self.item else {
return
}
var rectsSet: [[CGRect]] = []
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string {
let nsString = string as NSString
@ -827,7 +898,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
if i < self.linkPreviewHighlightingNodes.count {
textHighlightNode = self.linkPreviewHighlightingNodes[i]
} else {
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor.withMultipliedAlpha(0.5) : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor.withMultipliedAlpha(0.5))
self.linkPreviewHighlightingNodes.append(textHighlightNode)
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode)
}
@ -840,6 +911,77 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
}
}
private func updateLinkProgressState() {
guard let item = self.item else {
return
}
let range: NSRange = self.linkProgressRange ?? NSRange(location: NSNotFound, length: 0)
if range.location != NSNotFound {
let linkProgressView: TextLoadingEffectView
if let current = self.linkProgressView {
linkProgressView = current
} else {
linkProgressView = TextLoadingEffectView(frame: CGRect())
self.linkProgressView = linkProgressView
self.view.addSubview(linkProgressView)
}
linkProgressView.frame = self.textNode.textNode.frame
let progressColor: UIColor = item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor
linkProgressView.update(color: progressColor, textNode: self.textNode.textNode, range: range)
} else {
if let linkProgressView = self.linkProgressView {
self.linkProgressView = nil
linkProgressView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak linkProgressView] _ in
linkProgressView?.removeFromSuperview()
})
}
}
}
public func updateQuoteTextHighlightState(text: String?, animated: Bool) {
guard let item = self.item else {
return
}
var rectsSet: [[CGRect]] = []
if let text = text, !text.isEmpty, let cachedLayout = self.textNode.textNode.cachedLayout, let string = cachedLayout.attributedString?.string {
let nsString = string as NSString
let range = nsString.range(of: text)
if range.location != NSNotFound {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty {
rectsSet = [rects]
}
}
}
for i in 0 ..< rectsSet.count {
let rects = rectsSet[i]
let textHighlightNode: LinkHighlightingNode
if i < self.quoteHighlightingNodes.count {
textHighlightNode = self.quoteHighlightingNodes[i]
} else {
textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor)
self.quoteHighlightingNodes.append(textHighlightNode)
self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode)
}
textHighlightNode.frame = self.textNode.textNode.frame
textHighlightNode.updateRects(rects)
}
for i in (rectsSet.count ..< self.quoteHighlightingNodes.count).reversed() {
let node = self.quoteHighlightingNodes[i]
self.quoteHighlightingNodes.remove(at: i)
if animated {
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak node] _ in
node?.removeFromSupernode()
})
} else {
node.removeFromSupernode()
}
}
}
override public func willUpdateIsExtractedToContextPreview(_ value: Bool) {
if !value {
if let textSelectionNode = self.textSelectionNode {

View file

@ -42,29 +42,18 @@ public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleCon
let presentationData = item.presentationData
let insets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 9.0, right: 12.0)
let buttonImage: UIImage
let buttonHighlightedImage: UIImage
let titleColor: UIColor
let titleHighlightedColor: UIColor
if incoming {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)!
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill[0]
} else {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)!
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill[0]
}
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, nil, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, titleHighlightedColor, false)
let (buttonWidth, continueActionButtonLayout) = makeButtonLayout(constrainedSize.width, nil, false, presentationData.strings.Conversation_UpdateTelegram, titleColor, false, true)
let initialWidth = buttonWidth + insets.left + insets.right
return (initialWidth, { boundingWidth in
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
var actionButtonSizeAndApply: ((CGSize, (ListViewItemUpdateAnimation) -> ChatMessageAttachedContentButtonNode))?
let refinedButtonWidth = max(boundingWidth - insets.left - insets.right, buttonWidth)
let (size, apply) = continueActionButtonLayout(refinedButtonWidth, 33.0)
@ -76,7 +65,7 @@ public final class ChatMessageUnsupportedBubbleContentNode: ChatMessageBubbleCon
strongSelf.item = item
if let (size, apply) = actionButtonSizeAndApply {
_ = apply()
_ = apply(animation)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: insets.left, y: 0.0), size: size)
}
}

View file

@ -28,6 +28,7 @@ swift_library(
"//submodules/TelegramUI/Components/WallpaperPreviewMedia",
"//submodules/TelegramUI/Components/Chat/ChatMessageInteractiveMediaNode",
"//submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode",
"//submodules/TelegramUI/Components/Chat/ChatHistoryEntry",
],
visibility = [
"//visibility:public",

View file

@ -41,7 +41,7 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
self.contentNode.openMedia = { [weak self] mode in
if let strongSelf = self, let item = strongSelf.item {
if let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content {
if let _ = content.image, let _ = content.instantPage {
if let _ = content.instantPage {
if instantPageType(of: content) != .album {
item.controllerInteraction.openInstantPage(item.message, item.associatedData)
return
@ -86,7 +86,11 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
} else if webpage.instantPage != nil {
strongSelf.contentNode.openMedia?(.default)
} else {
item.controllerInteraction.openUrl(webpage.url, false, nil, nil)
var isConcealed = true
if item.message.text.contains(webpage.url) {
isConcealed = false
}
item.controllerInteraction.openUrl(webpage.url, isConcealed, nil, nil, nil)
}
}
}
@ -97,6 +101,16 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id, false)
}
}
self.contentNode.defaultContentAction = { [weak self] in
guard let self, let item = self.item, let webPage = self.webPage, case let .Loaded(content) = webPage.content else {
return .none
}
var isConcealed = true
if item.message.text.contains(content.url) {
isConcealed = false
}
return .url(url: content.url, concealed: isConcealed, activate: nil)
}
}
required public init?(coder aDecoder: NSCoder) {
@ -104,7 +118,8 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let contentNodeLayout = self.contentNode.asyncLayout()
let currentWebpage = self.webPage
let currentContentNodeLayout = self.contentNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize, _ in
var webPage: TelegramMediaWebpage?
@ -119,6 +134,16 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
}
var updatedContentNode: ChatMessageAttachedContentNode?
let contentNodeLayout: ChatMessageAttachedContentNode.AsyncLayout
if currentWebpage == nil || currentWebpage?.webpageId == webPage?.id {
contentNodeLayout = currentContentNodeLayout
} else {
let updatedContentNodeValue = ChatMessageAttachedContentNode()
updatedContentNode = updatedContentNodeValue
contentNodeLayout = updatedContentNodeValue.asyncLayout()
}
var title: String?
var subtitle: NSAttributedString?
var text: String?
@ -353,11 +378,15 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
}
if let largeMedia = webpage.displayOptions.largeMedia {
if largeMedia {
mediaAndFlags?.1.remove(.preferMediaInline)
} else {
mediaAndFlags?.1.insert(.preferMediaInline)
if let webPageContent, let isMediaLargeByDefault = webPageContent.isMediaLargeByDefault, !isMediaLargeByDefault {
mediaAndFlags?.1.insert(.preferMediaInline)
} else if let attribute = item.message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute {
if let forceLargeMedia = attribute.forceLargeMedia {
if forceLargeMedia {
mediaAndFlags?.1.remove(.preferMediaInline)
} else {
mediaAndFlags?.1.insert(.preferMediaInline)
}
}
}
} else if let adAttribute = item.message.adAttribute {
@ -410,13 +439,39 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
let (size, apply) = finalizeLayout(boundingWidth)
return (size, { [weak self] animation, synchronousLoads, applyInfo in
if let strongSelf = self {
strongSelf.item = item
strongSelf.webPage = webPage
guard let self else {
return
}
self.item = item
self.webPage = webPage
if let updatedContentNode {
let previousPosition = self.contentNode.position
let updatedPosition = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
do {
//animation.animator.updateScale(layer: self.contentNode.layer, scale: 0.9, completion: nil)
animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil)
animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 0.0, completion: { [weak contentNode] _ in
contentNode?.removeFromSupernode()
})
}
self.contentNode = updatedContentNode
self.addSubnode(updatedContentNode)
do {
apply(.None, synchronousLoads, applyInfo)
self.contentNode.frame = size.centered(around: previousPosition)
//animation.animator.animateScale(layer: self.contentNode.layer, from: 0.9, to: 1.0, completion: nil)
self.contentNode.alpha = 0.0
animation.animator.updateAlpha(layer: self.contentNode.layer, alpha: 1.0, completion: nil)
animation.animator.updatePosition(layer: self.contentNode.layer, position: updatedPosition, completion: nil)
}
} else {
self.contentNode.frame = CGRect(origin: CGPoint(), size: size)
apply(animation, synchronousLoads, applyInfo)
strongSelf.contentNode.frame = CGRect(origin: CGPoint(), size: size)
}
})
})
@ -472,9 +527,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
switch websiteType(of: content.websiteName) {
case .twitter:
return .url(url: "https://twitter.com/\(mention)", concealed: false)
return .url(url: "https://twitter.com/\(mention)", concealed: false, activate: nil)
case .instagram:
return .url(url: "https://instagram.com/\(mention)", concealed: false)
return .url(url: "https://instagram.com/\(mention)", concealed: false, activate: nil)
default:
break
}
@ -487,9 +542,9 @@ public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContent
}
switch websiteType(of: content.websiteName) {
case .twitter:
return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false)
return .url(url: "https://twitter.com/hashtag/\(hashtag)", concealed: false, activate: nil)
case .instagram:
return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false)
return .url(url: "https://instagram.com/explore/tags/\(hashtag)", concealed: false, activate: nil)
default:
break
}

View file

@ -274,9 +274,9 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, updateMessageReaction: { _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _ in
}, tapMessage: nil, clickThroughMessage: { }, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in return false }, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { [weak self] url, _, _, _, _ in
self?.openUrl(url)
}, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { [weak self] message, associatedData in
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {

View file

@ -239,7 +239,11 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
if let quote = strongSelf.quote {
//TODO:localize
titleText = "Reply to quote by \(authorName)"
strongSelf.textNode.attributedText = NSAttributedString(string: quote.text, font: textFont, textColor: strongSelf.theme.chat.inputPanel.primaryTextColor)
let textColor = strongSelf.theme.chat.inputPanel.primaryTextColor
let quoteText = stringWithAppliedEntities(trimToLineCount(quote.text, lineCount: 1), entities: quote.entities, baseColor: textColor, linkColor: textColor, baseFont: textFont, linkFont: textFont, boldFont: textFont, italicFont: textFont, boldItalicFont: textFont, fixedFont: textFont, blockQuoteFont: textFont, underlineLinks: false, message: message)
strongSelf.textNode.attributedText = quoteText
} else {
titleText = strongSelf.strings.Conversation_ReplyMessagePanelTitle(authorName).string
strongSelf.textNode.attributedText = messageText
@ -286,7 +290,7 @@ public final class ReplyAccessoryPanelNode: AccessoryPanelNode {
let text: String
//TODO:localize
if let (size, _, _) = strongSelf.validLayout, size.width > 320.0 {
text = "Tap here for options"
text = "Tap here for reply options"
} else {
text = "Tap here for forwarding options"
}

View file

@ -20,13 +20,11 @@ import MultiAnimationRenderer
public struct ChatInterfaceHighlightedState: Equatable {
public let messageStableId: UInt32
public let quote: String?
public init(messageStableId: UInt32) {
public init(messageStableId: UInt32, quote: String?) {
self.messageStableId = messageStableId
}
public static func ==(lhs: ChatInterfaceHighlightedState, rhs: ChatInterfaceHighlightedState) -> Bool {
return lhs.messageStableId == rhs.messageStableId
self.quote = quote
}
}
@ -75,6 +73,16 @@ public protocol ChatMessageTransitionProtocol: ASDisplayNode {
}
public struct NavigateToMessageParams {
public var timestamp: Double?
public var quote: String?
public init(timestamp: Double?, quote: String?) {
self.timestamp = timestamp
self.quote = quote
}
}
public final class ChatControllerInteraction {
public enum OpenPeerSource {
case `default`
@ -90,7 +98,7 @@ public final class ChatControllerInteraction {
public let openMessageReactionContextMenu: (Message, ContextExtractedContentContainingView, ContextGesture?, MessageReaction.Reaction) -> Void
public let activateMessagePinch: (PinchSourceContainerNode) -> Void
public let openMessageContextActions: (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void
public let navigateToMessage: (MessageId, MessageId) -> Void
public let navigateToMessage: (MessageId, MessageId, NavigateToMessageParams) -> Void
public let navigateToMessageStandalone: (MessageId) -> Void
public let navigateToThreadMessage: (PeerId, Int64, MessageId?) -> Void
public let tapMessage: ((Message) -> Void)?
@ -105,7 +113,7 @@ public final class ChatControllerInteraction {
public let requestMessageActionCallback: (MessageId, MemoryBuffer?, Bool, Bool) -> Void
public let requestMessageActionUrlAuth: (String, MessageActionUrlSubject) -> Void
public let activateSwitchInline: (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void
public let openUrl: (String, Bool, Bool?, Message?) -> Void
public let openUrl: (String, Bool, Bool?, Message?, Promise<Bool>?) -> Void
public let shareCurrentLocation: () -> Void
public let shareAccountContact: () -> Void
public let sendBotCommand: (MessageId?, String) -> Void
@ -204,7 +212,7 @@ public final class ChatControllerInteraction {
updateMessageReaction: @escaping (Message, ChatControllerInteractionReaction) -> Void,
activateMessagePinch: @escaping (PinchSourceContainerNode) -> Void,
openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void,
navigateToMessage: @escaping (MessageId, MessageId) -> Void,
navigateToMessage: @escaping (MessageId, MessageId, NavigateToMessageParams) -> Void,
navigateToMessageStandalone: @escaping (MessageId) -> Void,
navigateToThreadMessage: @escaping (PeerId, Int64, MessageId?) -> Void,
tapMessage: ((Message) -> Void)?,
@ -219,7 +227,7 @@ public final class ChatControllerInteraction {
requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool, Bool) -> Void,
requestMessageActionUrlAuth: @escaping (String, MessageActionUrlSubject) -> Void,
activateSwitchInline: @escaping (PeerId?, String, ReplyMarkupButtonAction.PeerTypes?) -> Void,
openUrl: @escaping (String, Bool, Bool?, Message?) -> Void,
openUrl: @escaping (String, Bool, Bool?, Message?, Promise<Bool>?) -> Void,
shareCurrentLocation: @escaping () -> Void,
shareAccountContact: @escaping () -> Void,
sendBotCommand: @escaping (MessageId?, String) -> Void,

View file

@ -1393,7 +1393,7 @@ public final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode,
chatControllerInteraction.toggleMessagesSelection(messageId, selected)
},
openUrl: { url, param1, param2, message in
chatControllerInteraction.openUrl(url, param1, param2, message)
chatControllerInteraction.openUrl(url, param1, param2, message, nil)
},
openInstantPage: { message, data in
chatControllerInteraction.openInstantPage(message, data)

View file

@ -1,82 +0,0 @@
import Foundation
import UIKit
public final class RichTextView: UIView {
public final class Params: Equatable {
let string: NSAttributedString
let constrainedSize: CGSize
public init(
string: NSAttributedString,
constrainedSize: CGSize
) {
self.string = string
self.constrainedSize = constrainedSize
}
public static func ==(lhs: Params, rhs: Params) -> Bool {
if !lhs.string.isEqual(to: rhs.string) {
return false
}
if lhs.constrainedSize != rhs.constrainedSize {
return false
}
return true
}
}
public final class LayoutData: Equatable {
init() {
}
public static func ==(lhs: LayoutData, rhs: LayoutData) -> Bool {
return true
}
}
public final class AsyncResult {
public let view: () -> RichTextView
public let layoutData: LayoutData
init(view: @escaping () -> RichTextView, layoutData: LayoutData) {
self.view = view
self.layoutData = layoutData
}
}
private static func performLayout(params: Params) -> LayoutData {
return LayoutData()
}
public static func updateAsync(_ view: RichTextView?) -> (Params) -> AsyncResult {
return { params in
let layoutData = performLayout(params: params)
return AsyncResult(
view: {
let view = view ?? RichTextView(frame: CGRect())
view.layoutData = layoutData
return view
},
layoutData: layoutData
)
}
}
private var layoutData: LayoutData?
override public init(frame: CGRect) {
super.init(frame: frame)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func draw(_ rect: CGRect) {
guard let layoutData = self.layoutData else {
return
}
let _ = layoutData
}
}

View file

@ -74,25 +74,6 @@ final class StoryItemLoadingEffectView: UIView {
UIGraphicsPopContext()
}
/*
let numColors = 7
var locations: [CGFloat] = []
var colors: [CGColor] = []
for i in 0 ..< numColors {
let position: CGFloat = CGFloat(i) / CGFloat(numColors - 1)
locations.append(position)
let distanceFromCenterFraction: CGFloat = max(0.0, min(1.0, abs(position - 0.5) / 0.5))
let colorAlpha = sin((1.0 - distanceFromCenterFraction) * CGFloat.pi * 0.5)
colors.append(foregroundColor.withMultipliedAlpha(colorAlpha).cgColor)
}
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: &locations)!
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions())*/
})
}
self.backgroundView.image = generateGradient(effectAlpha)
@ -109,11 +90,6 @@ final class StoryItemLoadingEffectView: UIView {
}
private func updateAnimations(size: CGSize) {
/*if "".isEmpty {
self.backgroundView.center = CGPoint(x: size.width * 0.5, y: size.height * 0.5)
return
}*/
if self.backgroundView.layer.animation(forKey: "shimmer") != nil || (self.playOnce && self.didPlayOnce) {
return
}

View file

@ -1,8 +1,8 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "RichTextView",
module_name = "RichTextView",
name = "TextLoadingEffect",
module_name = "TextLoadingEffect",
srcs = glob([
"Sources/**/*.swift",
]),
@ -10,6 +10,9 @@ swift_library(
"-warnings-as-errors",
],
deps = [
"//submodules/Display",
"//submodules/AppBundle",
"//submodules/Components/HierarchyTrackingLayer",
],
visibility = [
"//visibility:public",

View file

@ -0,0 +1,149 @@
import Foundation
import UIKit
import Display
import AppBundle
import HierarchyTrackingLayer
private let shadowImage: UIImage? = {
UIImage(named: "Stories/PanelGradient")
}()
public final class TextLoadingEffectView: UIView {
let hierarchyTrackingLayer: HierarchyTrackingLayer
private let maskContentsView: UIView
private let maskHighlightNode: LinkHighlightingNode
private let maskBorderContentsView: UIView
private let maskBorderHighlightNode: LinkHighlightingNode
private let backgroundView: UIImageView
private let borderBackgroundView: UIImageView
private let duration: Double
private let gradientWidth: CGFloat
private var size: CGSize?
override public init(frame: CGRect) {
self.hierarchyTrackingLayer = HierarchyTrackingLayer()
self.maskContentsView = UIView()
self.maskHighlightNode = LinkHighlightingNode(color: .black)
self.maskHighlightNode.useModernPathCalculation = true
self.maskBorderContentsView = UIView()
self.maskBorderHighlightNode = LinkHighlightingNode(color: .black)
self.maskBorderHighlightNode.borderOnly = true
self.maskBorderHighlightNode.useModernPathCalculation = true
self.maskBorderContentsView.addSubview(self.maskBorderHighlightNode.view)
self.backgroundView = UIImageView()
self.borderBackgroundView = UIImageView()
self.gradientWidth = 120.0
self.duration = 1.0
super.init(frame: frame)
self.isUserInteractionEnabled = false
self.maskContentsView.mask = self.maskHighlightNode.view
self.maskContentsView.addSubview(self.backgroundView)
self.addSubview(self.maskContentsView)
self.maskBorderContentsView.mask = self.maskBorderHighlightNode.view
self.maskBorderContentsView.addSubview(self.borderBackgroundView)
self.addSubview(self.maskBorderContentsView)
let generateGradient: (CGFloat) -> UIImage? = { baseAlpha in
return generateImage(CGSize(width: self.gradientWidth, height: 16.0), opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
let foregroundColor = UIColor(white: 1.0, alpha: min(1.0, baseAlpha * 4.0))
if let shadowImage {
UIGraphicsPushContext(context)
for i in 0 ..< 2 {
let shadowFrame = CGRect(origin: CGPoint(x: CGFloat(i) * (size.width * 0.5), y: 0.0), size: CGSize(width: size.width * 0.5, height: size.height))
context.saveGState()
context.translateBy(x: shadowFrame.midX, y: shadowFrame.midY)
context.rotate(by: CGFloat(i == 0 ? 1.0 : -1.0) * CGFloat.pi * 0.5)
let adjustedRect = CGRect(origin: CGPoint(x: -shadowFrame.height * 0.5, y: -shadowFrame.width * 0.5), size: CGSize(width: shadowFrame.height, height: shadowFrame.width))
context.clip(to: adjustedRect, mask: shadowImage.cgImage!)
context.setFillColor(foregroundColor.cgColor)
context.fill(adjustedRect)
context.restoreGState()
}
UIGraphicsPopContext()
}
})?.withRenderingMode(.alwaysTemplate)
}
self.backgroundView.image = generateGradient(0.5)
self.borderBackgroundView.image = generateGradient(1.0)
self.layer.addSublayer(self.hierarchyTrackingLayer)
self.hierarchyTrackingLayer.didEnterHierarchy = { [weak self] in
guard let self, let size = self.size else {
return
}
self.updateAnimations(size: size)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateAnimations(size: CGSize) {
if self.backgroundView.layer.animation(forKey: "shimmer") != nil {
return
}
let animation = self.backgroundView.layer.makeAnimation(from: 0.0 as NSNumber, to: (size.width + self.gradientWidth + size.width * 0.0) as NSNumber, keyPath: "position.x", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: self.duration, delay: 0.0, mediaTimingFunction: nil, removeOnCompletion: true, additive: true)
animation.repeatCount = Float.infinity
self.backgroundView.layer.add(animation, forKey: "shimmer")
self.borderBackgroundView.layer.add(animation, forKey: "shimmer")
}
public func update(color: UIColor, textNode: TextNode, range: NSRange) {
var rectsSet: [CGRect] = []
if let cachedLayout = textNode.cachedLayout {
if let rects = cachedLayout.rangeRects(in: range)?.rects, !rects.isEmpty {
rectsSet = rects
}
}
let maskFrame = CGRect(origin: CGPoint(), size: textNode.bounds.size).insetBy(dx: -4.0, dy: -4.0)
self.maskContentsView.backgroundColor = color.withAlphaComponent(0.1)
self.maskBorderContentsView.backgroundColor = color.withAlphaComponent(0.12)
self.backgroundView.tintColor = color
self.borderBackgroundView.tintColor = color
self.maskContentsView.frame = maskFrame
self.maskBorderContentsView.frame = maskFrame
self.maskHighlightNode.updateRects(rectsSet)
self.maskHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
self.maskBorderHighlightNode.updateRects(rectsSet)
self.maskBorderHighlightNode.frame = CGRect(origin: CGPoint(x: -maskFrame.minX, y: -maskFrame.minY), size: CGSize())
if self.size != maskFrame.size {
self.size = maskFrame.size
self.backgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
self.borderBackgroundView.frame = CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: maskFrame.height))
self.updateAnimations(size: maskFrame.size)
}
}
}

View file

@ -339,7 +339,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
return
}
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) })
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities, media: nil))).withoutSelectionState() }) })
f(.default)
})))
@ -381,7 +381,7 @@ private func generateChatReplyOptionItems(selfController: ChatControllerImpl, ch
return
}
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities))).withoutSelectionState() }) })
selfController.updateChatPresentationInterfaceState(animated: false, interactive: true, { $0.updatedInterfaceState({ $0.withUpdatedReplyMessageSubject(ChatInterfaceState.ReplyMessageSubject(messageId: replySubject.messageId, quote: EngineMessageReplyQuote(text: textSelection.text, entities: textSelection.entities, media: nil))).withoutSelectionState() }) })
f(.default)
})))
@ -603,7 +603,15 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
guard let peerId = selfController.chatLocation.peerId else {
return nil
}
guard let initialUrlPreview = selfController.presentationInterfaceState.urlPreview else {
let initialUrlPreview: ChatPresentationInterfaceState.UrlPreview?
if selfController.presentationInterfaceState.interfaceState.editMessage != nil {
initialUrlPreview = selfController.presentationInterfaceState.editingUrlPreview
} else {
initialUrlPreview = selfController.presentationInterfaceState.urlPreview
}
guard let initialUrlPreview else {
return nil
}
@ -612,23 +620,53 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
replySelectionState.get()
)
|> map { state, replySelectionState -> ChatControllerSubject.LinkOptions in
let urlPreview = state.urlPreview ?? initialUrlPreview
let urlPreview: ChatPresentationInterfaceState.UrlPreview
if state.interfaceState.editMessage != nil {
urlPreview = state.editingUrlPreview ?? initialUrlPreview
} else {
urlPreview = state.urlPreview ?? initialUrlPreview
}
var webpageOptions: TelegramMediaWebpageDisplayOptions = .default
var webpageHasLargeMedia = false
if case let .Loaded(content) = urlPreview.webPage.content {
if let isMediaLargeByDefault = content.isMediaLargeByDefault {
if isMediaLargeByDefault {
webpageHasLargeMedia = true
}
} else {
webpageHasLargeMedia = true
}
}
if let (_, webpage) = state.urlPreview, case let .Loaded(content) = webpage.content {
webpageOptions = content.displayOptions
let composeInputText: NSAttributedString = state.interfaceState.effectiveInputState.inputText
var replyMessageId: EngineMessage.Id?
var replyQuote: String?
if state.interfaceState.editMessage == nil {
replyMessageId = state.interfaceState.replyMessageSubject?.messageId
replyQuote = replySelectionState.quote?.text
}
let inputText = chatInputStateStringWithAppliedEntities(composeInputText.string, entities: generateChatInputTextEntities(composeInputText, generateLinks: false))
var largeMedia = false
if webpageHasLargeMedia {
largeMedia = urlPreview.largeMedia ?? true
} else {
largeMedia = false
}
return ChatControllerSubject.LinkOptions(
messageText: state.interfaceState.composeInputState.inputText.string,
messageEntities: generateChatInputTextEntities(state.interfaceState.composeInputState.inputText, generateLinks: true),
replyMessageId: state.interfaceState.replyMessageSubject?.messageId,
replyQuote: replySelectionState.quote?.text,
url: urlPreview.0,
webpage: urlPreview.1,
linkBelowText: webpageOptions.position != .aboveText,
largeMedia: webpageOptions.largeMedia != false
messageText: composeInputText.string,
messageEntities: generateChatInputTextEntities(composeInputText, generateLinks: true),
hasAlternativeLinks: detectUrls(inputText).count > 1,
replyMessageId: replyMessageId,
replyQuote: replyQuote,
url: urlPreview.url,
webpage: urlPreview.webPage,
linkBelowText: urlPreview.positionBelowText,
largeMedia: largeMedia
)
}
|> distinctUntilChanged
@ -640,9 +678,9 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
let items = linkOptions
|> deliverOnMainQueue
|> map { [weak selfController] linkOptions -> [ContextMenuItem] in
|> map { [weak selfController] linkOptions -> ContextController.Items in
guard let selfController else {
return []
return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list([]))
}
var items: [ContextMenuItem] = []
@ -657,15 +695,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
if state.interfaceState.editMessage != nil {
guard var urlPreview = state.editingUrlPreview else {
return state
}
urlPreview.positionBelowText = false
return state.updatedEditingUrlPreview(urlPreview)
} else {
guard var urlPreview = state.urlPreview else {
return state
}
urlPreview.positionBelowText = false
return state.updatedUrlPreview(urlPreview)
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.position = .aboveText
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
@ -677,20 +719,24 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
if state.interfaceState.editMessage != nil {
guard var urlPreview = state.editingUrlPreview else {
return state
}
urlPreview.positionBelowText = true
return state.updatedEditingUrlPreview(urlPreview)
} else {
guard var urlPreview = state.urlPreview else {
return state
}
urlPreview.positionBelowText = true
return state.updatedUrlPreview(urlPreview)
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.position = .belowText
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
}
if "".isEmpty {
if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault {
if !items.isEmpty {
items.append(.separator)
}
@ -705,15 +751,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
if state.interfaceState.editMessage != nil {
guard var urlPreview = state.editingUrlPreview else {
return state
}
urlPreview.largeMedia = false
return state.updatedEditingUrlPreview(urlPreview)
} else {
guard var urlPreview = state.urlPreview else {
return state
}
urlPreview.largeMedia = false
return state.updatedUrlPreview(urlPreview)
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.largeMedia = false
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
@ -725,15 +775,19 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
}
}, action: { [weak selfController] _, f in
selfController?.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
guard var urlPreview = state.urlPreview else {
return state
if state.interfaceState.editMessage != nil {
guard var urlPreview = state.editingUrlPreview else {
return state
}
urlPreview.largeMedia = true
return state.updatedEditingUrlPreview(urlPreview)
} else {
guard var urlPreview = state.urlPreview else {
return state
}
urlPreview.largeMedia = true
return state.updatedUrlPreview(urlPreview)
}
if case let .Loaded(content) = urlPreview.1.content {
var displayOptions = content.displayOptions
displayOptions.largeMedia = true
urlPreview = (urlPreview.0, TelegramMediaWebpage(webpageId: urlPreview.1.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
}
return state.updatedUrlPreview(urlPreview)
})
})))
}
@ -755,32 +809,44 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
f(.default)
})))
return items
return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items))
}
chatController.performOpenURL = { [weak selfController] message, url in
chatController.performOpenURL = { [weak selfController] message, url, progress in
guard let selfController else {
return
}
//TODO:
//func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? {
if let (updatedUrlPreviewUrl, signal) = urlPreviewStateForInputText(NSAttributedString(string: url), context: selfController.context, currentQuery: nil), let updatedUrlPreviewUrl {
progress?.set(.single(true))
let _ = (signal
|> afterDisposed {
progress?.set(.single(false))
}
|> deliverOnMainQueue).start(next: { [weak selfController] result in
guard let selfController else {
return
}
selfController.updateChatPresentationInterfaceState(animated: true, interactive: false, { state in
if let webpage = result(nil), var urlPreview = state.urlPreview {
if case let .Loaded(content) = urlPreview.1.content, case let .Loaded(newContent) = webpage.content {
urlPreview = (updatedUrlPreviewUrl, TelegramMediaWebpage(webpageId: webpage.webpageId, content: .Loaded(newContent.withDisplayOptions(content.displayOptions))))
if state.interfaceState.editMessage != nil {
if let webpage = result(nil), var urlPreview = state.editingUrlPreview {
urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage
return state.updatedEditingUrlPreview(urlPreview)
} else {
return state
}
return state.updatedUrlPreview(urlPreview)
} else {
return state
if let webpage = result(nil), var urlPreview = state.urlPreview {
urlPreview.url = updatedUrlPreviewUrl
urlPreview.webPage = webpage
return state.updatedUrlPreview(urlPreview)
} else {
return state
}
}
})
})
@ -792,7 +858,7 @@ private func chatLinkOptions(selfController: ChatControllerImpl, sourceNode: ASD
id: AnyHashable(OptionsId.link),
title: "Link",
source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
items: items |> map { ContextController.Items(content: .list($0)) }
items: items
)
}

View file

@ -140,7 +140,7 @@ enum ChatRecordingActivity {
}
public enum NavigateToMessageLocation {
case id(MessageId, Double?)
case id(MessageId, NavigateToMessageParams)
case index(MessageIndex)
case upperBound(PeerId)
@ -509,7 +509,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return .widthMultiplier(factor: 0.35, min: 16.0, max: 200.0)
}
var scheduledScrollToMessageId: (MessageId, Double?)?
var scheduledScrollToMessageId: (MessageId, NavigateToMessageParams)?
public var purposefulAction: (() -> Void)?
var updatedClosedPinnedMessageId: ((MessageId) -> Void)?
@ -553,7 +553,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var storyStats: PeerStoryStats?
var performTextSelectionAction: ((Message?, Bool, NSAttributedString, TextSelectionAction) -> Void)?
var performOpenURL: ((Message?, String) -> Void)?
var performOpenURL: ((Message?, String, Promise<Bool>?) -> Void)?
public init(context: AccountContext, chatLocation: ChatLocation, chatLocationContextHolder: Atomic<ChatLocationContextHolder?> = Atomic<ChatLocationContextHolder?>(value: nil), subject: ChatControllerSubject? = nil, botStart: ChatControllerInitialBotStart? = nil, attachBotStart: ChatControllerInitialAttachBotStart? = nil, botAppStart: ChatControllerInitialBotAppStart? = nil, mode: ChatControllerPresentationMode = .standard(previewing: false), peekData: ChatPeekTimeout? = nil, peerNearbyData: ChatPeerNearbyData? = nil, chatListFilter: Int32? = nil, chatNavigationStack: [ChatNavigationStackItem] = []) {
let _ = ChatControllerCount.modify { value in
@ -926,7 +926,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .pinnedMessageUpdated:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil))
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)))
break
}
}
@ -935,7 +935,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .gameScore:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, nil))
strongSelf.navigateToMessage(from: message.id, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: attribute.quote?.text)))
break
}
}
@ -1090,7 +1090,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .setSameChatWallpaper:
for attribute in message.attributes {
if let attribute = attribute as? ReplyMessageAttribute {
strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId)
strongSelf.controllerInteraction?.navigateToMessage(message.id, attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil))
return true
}
}
@ -2170,10 +2170,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.window?.presentInGlobalOverlay(pinchController)
}, openMessageContextActions: { message, node, rect, gesture in
gesture?.cancel()
}, navigateToMessage: { [weak self] fromId, id in
self?.navigateToMessage(from: fromId, to: .id(id, nil), forceInCurrentChat: fromId.peerId == id.peerId)
}, navigateToMessage: { [weak self] fromId, id, params in
self?.navigateToMessage(from: fromId, to: .id(id, params), forceInCurrentChat: fromId.peerId == id.peerId)
}, navigateToMessageStandalone: { [weak self] id in
self?.navigateToMessage(from: nil, to: .id(id, nil), forceInCurrentChat: false)
self?.navigateToMessage(from: nil, to: .id(id, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: false)
}, navigateToThreadMessage: { [weak self] peerId, threadId, messageId in
if let context = self?.context, let navigationController = self?.effectiveNavigationController {
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: threadId, messageId: messageId, navigationController: navigationController, activateInput: nil, keepStack: .always).startStandalone()
@ -2743,7 +2743,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.openPeer(peer: nil, navigation: .chat(textInputState: ChatTextInputState(inputText: NSAttributedString(string: inputString)), subject: nil, peekData: nil), fromMessage: nil, peerTypes: peerTypes)
}
}
}, openUrl: { [weak self] url, concealed, _, message in
}, openUrl: { [weak self] url, concealed, _, message, progress in
if let strongSelf = self {
var skipConcealedAlert = false
if let author = message?.author, author.isVerified {
@ -2755,8 +2755,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if let performOpenURL = strongSelf.performOpenURL {
performOpenURL(message, url)
performOpenURL(message, url, progress)
} else {
progress?.set(.single(false))
strongSelf.openUrl(url, concealed: concealed, skipConcealedAlert: skipConcealedAlert, message: message)
}
}
@ -2834,7 +2835,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
})
}
}, nil)
@ -3364,7 +3365,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
items.append(ActionSheetButtonItem(title: url.title, color: .accent, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
if let strongSelf = self {
strongSelf.controllerInteraction?.openUrl(url.url, false, false, message)
strongSelf.controllerInteraction?.openUrl(url.url, false, false, message, nil)
}
}))
}
@ -3797,7 +3798,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(Double(timestamp)))
} else {
strongSelf.navigateToMessage(messageLocation: .id(messageId, Double(timestamp)), animated: true, forceInCurrentChat: true)
strongSelf.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: Double(timestamp), quote: nil)), animated: true, forceInCurrentChat: true)
}
}
}
@ -3853,7 +3854,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
let inlineStickers: [MediaId: TelegramMediaFile] = [:]
strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in
strongSelf.editMessageDisposable.set((strongSelf.context.engine.messages.requestEditMessage(messageId: messageId, text: message.text, media: .keep, entities: entities, inlineStickers: inlineStickers, webpagePreviewAttribute: nil, disableUrlPreview: false, scheduleTime: time) |> deliverOnMainQueue).startStrict(next: { result in
}, error: { error in
}))
}
@ -3967,8 +3968,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let messageId = message?.id, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
var quoteData: EngineMessageReplyQuote?
let quoteText = (message.text as NSString).substring(with: NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound))
quoteData = EngineMessageReplyQuote(text: quoteText, entities: [])
let nsRange = NSRange(location: range.lowerBound, length: range.upperBound - range.lowerBound)
let quoteText = (message.text as NSString).substring(with: nsRange)
quoteData = EngineMessageReplyQuote(text: quoteText, entities: messageTextEntitiesInRange(entities: message.textEntitiesAttribute?.entities ?? [], range: nsRange, onlyQuoteable: true), media: nil)
let replySubject = ChatInterfaceState.ReplyMessageSubject(
messageId: message.id,
@ -4181,7 +4183,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.openMessageReplies(messageId: threadMessageId, displayProgressInMessage: message.id, isChannelPost: true, atMessage: attribute.messageId, displayModalProgress: false)
}
} else {
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, nil))
strongSelf.navigateToMessage(from: nil, to: .id(attribute.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)))
}
break
}
@ -4584,7 +4586,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .peer(id, messageId, startParam):
if case let .peer(currentPeerId) = self.chatLocation, currentPeerId == id {
if let messageId {
self.navigateToMessage(from: nil, to: .id(messageId, nil), rememberInStack: false)
self.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false)
}
} else {
let navigationData: ChatControllerInteractionNavigateToPeer
@ -4607,7 +4609,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .join(_, joinHash):
self.controllerInteraction?.openJoinLink(joinHash)
case let .webPage(_, url):
self.controllerInteraction?.openUrl(url, false, false, nil)
self.controllerInteraction?.openUrl(url, false, false, nil, nil)
}
}, openRequestedPeerSelection: { [weak self] messageId, peerType, buttonId in
guard let self else {
@ -5311,9 +5313,17 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case .reply:
//TODO:localize
subtitleTextSignal = .single("You can select a specific part to quote")
case .link:
//TODO:localize
subtitleTextSignal = .single("Tap on a link to generate its preview")
case let .link(link):
subtitleTextSignal = link.options
|> map { options -> String? in
if options.hasAlternativeLinks {
//TODO:localize
return "Tap on a link to generate its preview"
} else {
return nil
}
}
|> distinctUntilChanged
}
}
@ -6935,7 +6945,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
let location: ChatHistoryLocation
if let anchorMessageId = anchorMessageId {
location = .InitialSearch(location: .id(anchorMessageId), count: count, highlight: false)
location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false)
} else {
location = .Initial(count: count)
}
@ -8012,15 +8022,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.updatePlainInputSeparatorAlpha(plainInputSeparatorAlpha, transition: .animated(duration: 0.2, curve: .easeInOut))
}
self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toIndex, initial in
if let strongSelf = self, case let .message(index) = toIndex {
self.chatDisplayNode.historyNode.scrolledToIndex = { [weak self] toSubject, initial in
if let strongSelf = self, case let .message(index) = toSubject.index {
if case let .message(messageSubject, _, _) = strongSelf.subject, initial, case let .id(messageId) = messageSubject, messageId != index.id {
if messageId.peerId == index.id.peerId {
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: strongSelf.presentationData.strings.Conversation_MessageDoesntExist, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
} else if let controllerInteraction = strongSelf.controllerInteraction {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(index.id) {
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId)
let highlightedState = ChatInterfaceHighlightedState(messageStableId: message.stableId, quote: toSubject.quote)
controllerInteraction.highlightedState = highlightedState
strongSelf.updateItemNodesHighlightedStates(animated: initial)
strongSelf.scrolledToMessageIdValue = ScrolledToMessageId(id: index.id, allowedReplacementDirection: [])
@ -8034,9 +8044,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}))
if let (messageId, maybeTimecode) = strongSelf.scheduledScrollToMessageId {
if let quote = toSubject.quote, !message.text.contains(quote) {
//TODO:localize
strongSelf.present(UndoOverlayController(presentationData: strongSelf.presentationData, content: .info(title: nil, text: "Quote not found", timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
}
if let (messageId, params) = strongSelf.scheduledScrollToMessageId {
strongSelf.scheduledScrollToMessageId = nil
if let timecode = maybeTimecode, message.id == messageId {
if let timecode = params.timestamp, message.id == messageId {
Queue.mainQueue().after(0.2) {
let _ = strongSelf.controllerInteraction?.openMessage(message, .timecode(timecode))
}
@ -8334,18 +8349,28 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.chatDisplayNode.dismissUrlPreview = { [weak self] in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.interfaceState.editMessage {
if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInterfaceState {
$0.withUpdatedEditMessage($0.editMessage.flatMap { $0.withUpdatedDisableUrlPreview(link) })
if let link = strongSelf.presentationInterfaceState.editingUrlPreview?.url {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in
return presentationInterfaceState.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEditMessage(interfaceState.editMessage.flatMap { editMessage in
var editMessage = editMessage
if !editMessage.disableUrlPreviews.contains(link) {
editMessage.disableUrlPreviews.append(link)
}
return editMessage
})
}
})
}
} else {
if let (link, _) = strongSelf.presentationInterfaceState.urlPreview {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
$0.updatedInterfaceState {
$0.withUpdatedComposeDisableUrlPreview(link)
if let link = strongSelf.presentationInterfaceState.urlPreview?.url {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { presentationInterfaceState in
return presentationInterfaceState.updatedInterfaceState { interfaceState in
var composeDisableUrlPreviews = interfaceState.composeDisableUrlPreviews
if !composeDisableUrlPreviews.contains(link) {
composeDisableUrlPreviews.append(link)
}
return interfaceState.withUpdatedComposeDisableUrlPreviews(composeDisableUrlPreviews)
}
})
}
@ -8356,7 +8381,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.chatDisplayNode.navigateButtons.downPressed = { [weak self] in
if let strongSelf = self, strongSelf.isNodeLoaded {
if let messageId = strongSelf.historyNavigationStack.removeLast() {
strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, nil), rememberInStack: false)
strongSelf.navigateToMessage(from: nil, to: .id(messageId.id, NavigateToMessageParams(timestamp: nil, quote: nil)), rememberInStack: false)
} else {
if case .known = strongSelf.chatDisplayNode.historyNode.visibleContentOffset() {
strongSelf.chatDisplayNode.historyNode.scrollToEndOfHistory()
@ -8379,7 +8404,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
switch result {
case let .result(messageId):
if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil))
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)))
}
case .loading:
break
@ -8437,7 +8462,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
case let .result(messageId):
if let messageId = messageId {
strongSelf.chatDisplayNode.historyNode.suspendReadingReactions = true
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), scrollPosition: .center(.top), completion: {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), scrollPosition: .center(.top), completion: {
guard let strongSelf = self else {
return
}
@ -8640,29 +8665,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let _ = strongSelf.presentVoiceMessageDiscardAlert(action: {
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { state in
var updated = state.updatedInterfaceState {
var entities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
break
}
var entities: [MessageTextEntity] = []
for attribute in message.attributes {
if let attribute = attribute as? TextEntitiesMessageAttribute {
entities = attribute.entities
break
}
var inputTextMaxLength: Int32 = 4096
var webpageUrl: String?
for media in message.media {
if media is TelegramMediaImage || media is TelegramMediaFile {
inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
webpageUrl = content.url
}
}
var inputTextMaxLength: Int32 = 4096
var webpageUrl: String?
for media in message.media {
if media is TelegramMediaImage || media is TelegramMediaFile {
inputTextMaxLength = strongSelf.context.userLimits.maxCaptionLength
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
webpageUrl = content.url
}
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
var disableUrlPreview: String?
if let detectedWebpageUrl = detectUrl(inputText), webpageUrl == nil {
disableUrlPreview = detectedWebpageUrl
}
return $0.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreview: disableUrlPreview, inputTextMaxLength: inputTextMaxLength))
}
let inputText = chatInputStateStringWithAppliedEntities(message.text, entities: entities)
var disableUrlPreviews: [String] = []
if webpageUrl == nil {
disableUrlPreviews = detectUrls(inputText)
}
var updated = state.updatedInterfaceState { interfaceState in
return interfaceState.withUpdatedEditMessage(ChatEditMessageState(messageId: messageId, inputState: ChatTextInputState(inputText: inputText), disableUrlPreviews: disableUrlPreviews, inputTextMaxLength: inputTextMaxLength))
}
updated = updatedChatEditInterfaceMessageState(state: updated, message: message)
@ -8967,9 +8994,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, editMessage: { [weak self] in
if let strongSelf = self, let editMessage = strongSelf.presentationInterfaceState.interfaceState.editMessage {
var disableUrlPreview = false
if let (link, _) = strongSelf.presentationInterfaceState.editingUrlPreview {
if editMessage.disableUrlPreview == link {
var webpage: TelegramMediaWebpage?
var webpagePreviewAttribute: WebpagePreviewMessageAttribute?
if let urlPreview = strongSelf.presentationInterfaceState.editingUrlPreview {
if editMessage.disableUrlPreviews.contains(urlPreview.url) {
disableUrlPreview = true
} else {
webpage = urlPreview.webPage
webpagePreviewAttribute = WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true)
}
}
@ -9022,6 +9055,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let editMediaReference = strongSelf.presentationInterfaceState.editMessageState?.mediaReference {
media = .update(editMediaReference)
updatingMedia = true
} else if let webpage {
media = .update(.standalone(media: webpage))
} else {
media = .keep
}
@ -9032,8 +9067,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
if let currentMessage = currentMessage {
let currentEntities = currentMessage.textEntitiesAttribute?.entities ?? []
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || disableUrlPreview {
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, disableUrlPreview: disableUrlPreview)
let currentWebpagePreviewAttribute = currentMessage.webpagePreviewAttribute ?? WebpagePreviewMessageAttribute(leadingPreview: false, forceLargeMedia: nil, isManuallyAdded: true)
if currentMessage.text != text.string || currentEntities != entities || updatingMedia || webpagePreviewAttribute != currentWebpagePreviewAttribute || disableUrlPreview {
strongSelf.context.account.pendingUpdateMessageManager.add(messageId: editMessage.messageId, text: text.string, media: media, entities: entitiesAttribute, inlineStickers: inlineStickers, webpagePreviewAttribute: webpagePreviewAttribute, disableUrlPreview: disableUrlPreview)
}
}
@ -9197,7 +9234,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateItemNodesSearchTextHighlightStates()
}
}, navigateToMessage: { [weak self] messageId, dropStack, forceInCurrentChat, statusSubject in
self?.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject)
self?.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: forceInCurrentChat, dropStack: dropStack, statusSubject: statusSubject)
}, navigateToChat: { [weak self] peerId in
guard let strongSelf = self else {
return
@ -9261,7 +9298,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.chatDisplayNode.collapseInput()
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreview(nil) }
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))).withUpdatedComposeDisableUrlPreviews([]) }
})
}
}, nil)
@ -12264,8 +12301,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
inScopeResult = result
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.1) {
return $0.updatedUrlPreview((updatedUrlPreviewUrl, webpage))
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = result($0.urlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: updatedUrlPreviewUrl,
webPage: webpage,
positionBelowText: $0.urlPreview?.positionBelowText ?? true,
largeMedia: $0.urlPreview?.largeMedia
)
return $0.updatedUrlPreview(updatedPreview)
} else {
return $0.updatedUrlPreview(nil)
}
@ -12275,8 +12318,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
inScope = false
if let inScopeResult = inScopeResult {
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.1) {
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview((updatedUrlPreviewUrl, webpage))
if let updatedUrlPreviewUrl = updatedUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.urlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: updatedUrlPreviewUrl,
webPage: webpage,
positionBelowText: updatedChatPresentationInterfaceState.urlPreview?.positionBelowText ?? true,
largeMedia: updatedChatPresentationInterfaceState.urlPreview?.largeMedia
)
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(updatedPreview)
} else {
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedUrlPreview(nil)
}
@ -12296,8 +12345,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
inScopeResult = result
} else {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.1) {
return $0.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage))
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = result($0.editingUrlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: updatedEditingUrlPreviewUrl,
webPage: webpage,
positionBelowText: $0.editingUrlPreview?.positionBelowText ?? true,
largeMedia: $0.editingUrlPreview?.largeMedia
)
return $0.updatedEditingUrlPreview(updatedPreview)
} else {
return $0.updatedEditingUrlPreview(nil)
}
@ -12307,8 +12362,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
inScope = false
if let inScopeResult = inScopeResult {
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.1) {
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview((updatedEditingUrlPreviewUrl, webpage))
if let updatedEditingUrlPreviewUrl = updatedEditingUrlPreviewUrl, let webpage = inScopeResult(updatedChatPresentationInterfaceState.editingUrlPreview?.webPage) {
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: updatedEditingUrlPreviewUrl,
webPage: webpage,
positionBelowText: updatedChatPresentationInterfaceState.editingUrlPreview?.positionBelowText ?? true,
largeMedia: updatedChatPresentationInterfaceState.editingUrlPreview?.largeMedia
)
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(updatedPreview)
} else {
updatedChatPresentationInterfaceState = updatedChatPresentationInterfaceState.updatedEditingUrlPreview(nil)
}
@ -15708,7 +15769,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var interfaceState = interfaceState
interfaceState = interfaceState.withUpdatedReplyMessageSubject(nil)
interfaceState = interfaceState.withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: "")))
interfaceState = interfaceState.withUpdatedComposeDisableUrlPreview(nil)
interfaceState = interfaceState.withUpdatedComposeDisableUrlPreviews([])
return interfaceState
}
}
@ -16237,7 +16298,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
func scrollToEndOfHistory() {
let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0)
let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: 0)
let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView
@ -16301,7 +16362,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
func scrollToStartOfHistory() {
let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0)
let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: 0)
let historyView = preloadedChatHistoryViewForLocation(locationInput, context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView
@ -16424,7 +16485,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
strongSelf.loadingMessage.set(.single(nil))
if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true)
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true)
}
}
}))
@ -16461,7 +16522,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
strongSelf.loadingMessage.set(.single(nil))
if let messageId = messageId {
strongSelf.navigateToMessage(from: nil, to: .id(messageId, nil), forceInCurrentChat: true)
strongSelf.navigateToMessage(from: nil, to: .id(messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), forceInCurrentChat: true)
}
}
}))
@ -16718,7 +16779,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
chatLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: nil), keepStack: .always))
var quote: String?
if case let .id(_, params) = messageLocation {
quote = params.quote
}
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: self.context, chatLocation: chatLocation, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil), keepStack: .always))
}
})
} else if forceInCurrentChat {
@ -16745,7 +16811,11 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
delayCompletion = false
}
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, scrollPosition: scrollPosition)
var quote: String?
if case let .id(_, params) = messageLocation {
quote = params.quote
}
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: message.index, animated: animated, quote: quote, scrollPosition: scrollPosition)
if delayCompletion {
Queue.mainQueue().after(0.25, {
@ -16757,14 +16827,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
if case let .id(_, maybeTimecode) = messageLocation, let timecode = maybeTimecode {
if case let .id(_, params) = messageLocation, let timecode = params.timestamp {
let _ = self.controllerInteraction?.openMessage(message, .timecode(timecode))
}
} else if case let .index(index) = messageLocation, index.id.id == 0, index.timestamp > 0, case .scheduledMessages = self.presentationInterfaceState.subject {
self.chatDisplayNode.historyNode.scrollToMessage(from: scrollFromIndex, to: index, animated: animated, scrollPosition: scrollPosition)
} else {
if case let .id(messageId, maybeTimecode) = messageLocation, let timecode = maybeTimecode {
self.scheduledScrollToMessageId = (messageId, timecode)
if case let .id(messageId, params) = messageLocation, params.timestamp != nil {
self.scheduledScrollToMessageId = (messageId, params)
}
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
let searchLocation: ChatHistoryInitialSearchLocation
@ -16780,7 +16850,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
searchLocation = .index(.absoluteUpperBound())
}
}
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView
|> mapToSignal { historyView -> Signal<(MessageIndex?, Bool), NoError> in
@ -16877,7 +16947,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.historyNavigationStack.add(fromIndex)
}
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(location: searchLocation, count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
var quote: String?
if case let .id(_, params) = messageLocation {
quote = params.quote
}
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote), count: 50, highlight: true), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tagMask: nil, additionalData: [])
let signal = historyView
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
switch historyView {
@ -17789,7 +17865,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if case .peer(peerId.id) = strongSelf.chatLocation {
if let subject = subject, case let .message(messageSubject, _, timecode) = subject {
if case let .id(messageId) = messageSubject {
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, timecode))
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil)))
}
} else {
self?.playShakeAnimation()
@ -17863,6 +17939,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
func openUrl(_ url: String, concealed: Bool, forceExternal: Bool = false, skipUrlAuth: Bool = false, skipConcealedAlert: Bool = false, message: Message? = nil, commit: @escaping () -> Void = {}) {
self.commitPurposefulAction()
if let message, let webpage = message.media.first(where: { $0 is TelegramMediaWebpage }) as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content, content.url == url {
if let navigationController = self.navigationController as? NavigationController {
self.context.sharedContext.openChatInstantPage(context: self.context, message: message, sourcePeerType: nil, navigationController: navigationController)
return
}
}
let _ = self.presentVoiceMessageDiscardAlert(action: {
openUserGeneratedUrl(context: self.context, peerId: self.peerView?.peerId, url: url, concealed: concealed, skipUrlAuth: skipUrlAuth, skipConcealedAlert: skipConcealedAlert, present: { [weak self] c in
self?.present(c, in: .window(.root))
@ -18569,7 +18652,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return updatedState
})
strongSelf.navigateToMessage(messageLocation: .id(message.id, nil), animated: true)
strongSelf.navigateToMessage(messageLocation: .id(message.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true)
}
} else {
strongSelf.scrollToEndOfHistory()
@ -18591,7 +18674,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
if let lastMessage = lastMessage {
strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, nil), animated: true)
strongSelf.navigateToMessage(messageLocation: .id(lastMessage.id, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true)
}
})
}
@ -18630,7 +18713,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
if let updatedReplyMessageSubject {
strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, nil), animated: true)
strongSelf.navigateToMessage(messageLocation: .id(updatedReplyMessageSubject.messageId, NavigateToMessageParams(timestamp: nil, quote: nil)), animated: true)
}
}
}

View file

@ -528,32 +528,21 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
var media: [Media] = []
if case let .Loaded(content) = options.webpage.content {
var displayOptions: TelegramMediaWebpageDisplayOptions = .default
if options.linkBelowText {
displayOptions.position = .belowText
} else {
displayOptions.position = .aboveText
}
if options.largeMedia {
displayOptions.largeMedia = true
} else {
displayOptions.largeMedia = false
}
media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content.withDisplayOptions(displayOptions))))
media.append(TelegramMediaWebpage(webpageId: options.webpage.webpageId, content: .Loaded(content)))
}
var attributes: [MessageAttribute] = []
attributes.append(TextEntitiesMessageAttribute(entities: options.messageEntities))
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !options.linkBelowText, forceLargeMedia: options.largeMedia, isManuallyAdded: true))
if let replyMessage {
associatedMessages[replyMessage.id] = replyMessage
var mappedQuote: EngineMessageReplyQuote?
if let quote = options.replyQuote {
mappedQuote = EngineMessageReplyQuote(text: quote, entities: [])
mappedQuote = EngineMessageReplyQuote(text: quote, entities: [], media: nil)
}
attributes.append(ReplyMessageAttribute(messageId: replyMessage.id, threadMessageId: nil, quote: mappedQuote))
@ -3411,10 +3400,13 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
var webpage: TelegramMediaWebpage?
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
webpage = self.chatPresentationInterfaceState.urlPreview?.1
if let urlPreview = self.chatPresentationInterfaceState.urlPreview {
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
} else {
webpage = urlPreview.webPage
attributes.append(WebpagePreviewMessageAttribute(leadingPreview: !urlPreview.positionBelowText, forceLargeMedia: urlPreview.largeMedia, isManuallyAdded: true))
}
}
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
@ -3488,7 +3480,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
strongSelf.ignoreUpdateHeight = true
textInputPanelNode.text = ""
strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreview(nil) })
strongSelf.requestUpdateChatInterfaceState(.immediate, true, { $0.withUpdatedReplyMessageSubject(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeDisableUrlPreviews([]) })
strongSelf.ignoreUpdateHeight = false
}
}, usedCorrelationId)

View file

@ -54,7 +54,7 @@ public enum ChatHistoryListMode: Equatable {
enum ChatHistoryViewScrollPosition {
case unread(index: MessageIndex)
case positionRestoration(index: MessageIndex, relativeOffset: CGFloat)
case index(index: MessageHistoryAnchorIndex, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool)
case index(subject: MessageHistoryScrollToSubject, position: ListViewScrollPosition, directionHint: ListViewScrollToItemDirectionHint, animated: Bool, highlight: Bool, displayLink: Bool)
}
enum ChatHistoryViewUpdateType {
@ -125,7 +125,7 @@ struct ChatHistoryViewTransition {
var cachedData: CachedPeerData?
var cachedDataMessages: [MessageId: Message]?
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
var scrolledToIndex: MessageHistoryAnchorIndex?
var scrolledToIndex: MessageHistoryScrollToSubject?
var scrolledToSomeIndex: Bool
var animateIn: Bool
var reason: ChatHistoryViewTransitionReason
@ -145,7 +145,7 @@ struct ChatHistoryListViewTransition {
var cachedData: CachedPeerData?
var cachedDataMessages: [MessageId: Message]?
var readStateData: [PeerId: ChatHistoryCombinedInitialReadStateData]?
var scrolledToIndex: MessageHistoryAnchorIndex?
var scrolledToIndex: MessageHistoryScrollToSubject?
var scrolledToSomeIndex: Bool
var peerType: MediaAutoDownloadPeerType
var networkType: MediaAutoDownloadNetworkType
@ -206,7 +206,7 @@ extension ListMessageItemInteraction {
}, toggleMessagesSelection: { messageId, selected in
controllerInteraction.toggleMessagesSelection(messageId, selected)
}, openUrl: { url, param1, param2, message in
controllerInteraction.openUrl(url, param1, param2, message)
controllerInteraction.openUrl(url, param1, param2, message, nil)
}, openInstantPage: { message, data in
controllerInteraction.openInstantPage(message, data)
}, longTap: { action, message in
@ -381,10 +381,14 @@ private extension ChatHistoryLocationInput {
switch self.content {
case .Navigation(index: .upperBound, anchorIndex: .upperBound, count: _, highlight: _):
return true
case .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: _, scrollPosition: _, animated: _, highlight: _):
case let .Scroll(subject, anchorIndex, _, _, _, _):
if case .upperBound = anchorIndex, case .upperBound = subject.index {
return true
default:
} else {
return false
}
default:
return false
}
}
}
@ -532,7 +536,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var maxVisibleMessageIndexReported: MessageIndex?
var maxVisibleMessageIndexUpdated: ((MessageIndex) -> Void)?
var scrolledToIndex: ((MessageHistoryAnchorIndex, Bool) -> Void)?
var scrolledToIndex: ((MessageHistoryScrollToSubject, Bool) -> Void)?
var scrolledToSomeIndex: (() -> Void)?
var beganDragging: (() -> Void)?
@ -812,9 +816,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(MessageIndex.absoluteUpperBound())
}
}
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: 0)
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: highlight?.quote), count: historyMessageCount, highlight: highlight != nil), id: 0)
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: 0)
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: 0)
} else {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0)
}
@ -1092,7 +1096,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
let scrollPosition: ChatHistoryViewScrollPosition?
if isFirstTime, let messageIndex = messages.first(where: { $0.id == at })?.index {
scrollPosition = .index(index: .message(messageIndex), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false)
scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(messageIndex), quote: nil), position: .center(.bottom), directionHint: .Down, animated: false, highlight: false, displayLink: false)
isFirstTime = false
} else {
scrollPosition = nil
@ -1370,9 +1374,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
initialSearchLocation = .index(.absoluteUpperBound())
}
}
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: initialSearchLocation, count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: nil), count: historyMessageCount, highlight: highlight != nil), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(location: .id(messageId), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
} else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue {
chatHistoryLocation.id += 1
strongSelf.chatHistoryLocationValue = chatHistoryLocation
@ -1508,10 +1512,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if scrollPosition == nil, let originalScrollPosition = originalScrollPosition {
switch originalScrollPosition {
case let .index(index, position, _, _, highlight, displayLink):
if case .upperBound = index {
case let .index(subject, position, _, _, highlight, displayLink):
if case .upperBound = subject.index {
if let previous = previous, previous.filteredEntries.isEmpty {
updatedScrollPosition = .index(index: index, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink)
updatedScrollPosition = .index(subject: subject, position: position, directionHint: .Down, animated: false, highlight: highlight, displayLink: displayLink)
}
}
default:
@ -1558,7 +1562,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if let strongSelf = self, case .default = source {
strongSelf.toLang = translateToLanguage
if strongSelf.appliedScrollToMessageId == nil, let scrollToMessageId = scrollToMessageId {
updatedScrollPosition = .index(index: .message(scrollToMessageId), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true)
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(scrollToMessageId), quote: nil), position: .center(.top), directionHint: .Up, animated: true, highlight: false, displayLink: true)
scrollAnimationCurve = .Spring(duration: 0.4)
} else {
let wasPlaying = strongSelf.appliedPlayingMessageId != nil
@ -1588,7 +1592,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
})
if currentIsVisible && nextIsVisible && currentlyPlayingVideo {
updatedScrollPosition = .index(index: .message(currentlyPlayingMessageId), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true)
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(currentlyPlayingMessageId), quote: nil), position: .center(.bottom), directionHint: .Up, animated: true, highlight: true, displayLink: true)
scrollAnimationCurve = .Spring(duration: 0.4)
}
}
@ -1633,7 +1637,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
if fillsScreen, let firstNonAdIndex = firstNonAdIndex, previousNumAds == 0, updatedNumAds != 0 {
updatedScrollPosition = .index(index: .message(firstNonAdIndex), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false)
updatedScrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(firstNonAdIndex), quote: nil), position: .top(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false)
disableAnimations = true
}
}
@ -2573,7 +2577,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
currentMessage = messages.first?.0
}
if let message = currentMessage, let _ = self.anchorMessageInCurrentHistoryView() {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(message.index), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(message.index), quote: nil), anchorIndex: .message(message.index), sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
}
}
}
@ -2602,14 +2606,14 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
if let currentMessage = currentMessage {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(currentMessage.index), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId())
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(currentMessage.index), quote: nil), anchorIndex: .message(currentMessage.index), sourceIndex: .upperBound, scrollPosition: .top(0.0), animated: true, highlight: true), id: self.takeNextHistoryLocationId())
}
}
}
public func scrollToStartOfHistory() {
self.beganDragging?()
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .lowerBound, anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .lowerBound, quote: nil), anchorIndex: .lowerBound, sourceIndex: .upperBound, scrollPosition: .bottom(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
}
public func scrollToEndOfHistory() {
@ -2618,13 +2622,13 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
case let .known(value) where value <= CGFloat.ulpOfOne:
break
default:
let locationInput = ChatHistoryLocationInput(content: .Scroll(index: .upperBound, anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
let locationInput = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .upperBound, quote: nil), anchorIndex: .upperBound, sourceIndex: .lowerBound, scrollPosition: .top(0.0), animated: true, highlight: false), id: self.takeNextHistoryLocationId())
self.chatHistoryLocationValue = locationInput
}
}
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, scrollPosition: ListViewScrollPosition = .center(.bottom)) {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(index: .message(toIndex), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId())
public func scrollToMessage(from fromIndex: MessageIndex, to toIndex: MessageIndex, animated: Bool, highlight: Bool = true, quote: String? = nil, scrollPosition: ListViewScrollPosition = .center(.bottom)) {
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Scroll(subject: MessageHistoryScrollToSubject(index: .message(toIndex), quote: quote), anchorIndex: .message(toIndex), sourceIndex: .message(fromIndex), scrollPosition: scrollPosition, animated: animated, highlight: highlight), id: self.takeNextHistoryLocationId())
}
public func anchorMessageInCurrentHistoryView() -> Message? {

View file

@ -41,9 +41,9 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
if scheduled {
var first = true
var chatScrollPosition: ChatHistoryViewScrollPosition?
if case let .Scroll(index, _, sourceIndex, position, animated, highlight) = location.content {
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
chatScrollPosition = .index(index: index, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false)
if case let .Scroll(subject, _, sourceIndex, position, animated, highlight) = location.content {
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up
chatScrollPosition = .index(subject: subject, position: position, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false)
}
return account.viewTracker.scheduledMessagesViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), additionalData: additionalData)
|> map { view, updateType, initialData -> ChatHistoryViewUpdate in
@ -112,7 +112,7 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
if tagMask == nil, case let .replyThread(message) = chatLocation, message.isForumPost, view.maxReadIndex == nil {
if case let .message(index) = view.anchorIndex {
scrollPosition = .index(index: .message(index), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false)
scrollPosition = .index(subject: MessageHistoryScrollToSubject(index: .message(index), quote: nil), position: .bottom(0.0), directionHint: .Up, animated: false, highlight: false, displayLink: false)
}
}
@ -174,12 +174,12 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
return .HistoryView(view: view, type: .Initial(fadeIn: fadeIn), scrollPosition: scrollPosition, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
}
case let .InitialSearch(searchLocation, count, highlight):
case let .InitialSearch(searchLocationSubject, count, highlight):
var preloaded = false
var fadeIn = false
let signal: Signal<(MessageHistoryView, ViewUpdateType, InitialMessageHistoryData?), NoError>
switch searchLocation {
switch searchLocationSubject.location {
case let .index(index):
signal = account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: .message(index), anchorIndex: .message(index), count: count, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: nil, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
case let .id(id):
@ -226,7 +226,8 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
}
preloaded = true
return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(index: anchorIndex, position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
}
case let .Navigation(index, anchorIndex, count, _):
@ -243,11 +244,11 @@ func chatHistoryViewForLocation(_ location: ChatHistoryLocationInput, ignoreMess
}
return .HistoryView(view: view, type: .Generic(type: genericType), scrollPosition: nil, flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
}
case let .Scroll(index, anchorIndex, sourceIndex, scrollPosition, animated, highlight):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > index ? .Down : .Up
let chatScrollPosition = ChatHistoryViewScrollPosition.index(index: index, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false)
case let .Scroll(subject, anchorIndex, sourceIndex, scrollPosition, animated, highlight):
let directionHint: ListViewScrollToItemDirectionHint = sourceIndex > subject.index ? .Down : .Up
let chatScrollPosition = ChatHistoryViewScrollPosition.index(subject: subject, position: scrollPosition, directionHint: directionHint, animated: animated, highlight: highlight, displayLink: false)
var first = true
return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
return account.viewTracker.aroundMessageHistoryViewForLocation(context.chatLocationInput(for: chatLocation, contextHolder: chatLocationContextHolder), ignoreMessagesInTimestampRange: ignoreMessagesInTimestampRange, index: subject.index, anchorIndex: anchorIndex, count: 128, ignoreRelatedChats: ignoreRelatedChats, fixedCombinedReadStates: fixedCombinedReadStates, tagMask: tagMask, appendMessagesFromTheSameGroup: appendMessagesFromTheSameGroup, orderStatistics: orderStatistics, additionalData: additionalData)
|> map { view, updateType, initialData -> ChatHistoryViewUpdate in
let (cachedData, cachedDataMessages, readStateData) = extractAdditionalData(view: view, chatLocation: chatLocation)
@ -359,7 +360,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
case .automatic:
if let atMessageId = atMessageId {
input = ChatHistoryLocationInput(
content: .InitialSearch(location: .id(atMessageId), count: 40, highlight: true),
content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true),
id: 0
)
} else {

View file

@ -28,14 +28,14 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
}
if let editMessage = chatPresentationInterfaceState.interfaceState.editMessage {
if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, editMessage.disableUrlPreview != editingUrlPreview.0 {
if let editingUrlPreview = chatPresentationInterfaceState.editingUrlPreview, !editMessage.disableUrlPreviews.contains(editingUrlPreview.url) {
if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode {
previewPanelNode.interfaceInteraction = interfaceInteraction
previewPanelNode.replaceWebpage(url: editingUrlPreview.0, webpage: editingUrlPreview.1)
previewPanelNode.replaceWebpage(url: editingUrlPreview.url, webpage: editingUrlPreview.webPage)
previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return previewPanelNode
} else {
let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.0, webpage: editingUrlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: editingUrlPreview.url, webpage: editingUrlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
}
@ -50,14 +50,14 @@ func accessoryPanelForChatPresentationIntefaceState(_ chatPresentationInterfaceS
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
}
} else if let urlPreview = chatPresentationInterfaceState.urlPreview, chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != urlPreview.0 {
} else if let urlPreview = chatPresentationInterfaceState.urlPreview, !chatPresentationInterfaceState.interfaceState.composeDisableUrlPreviews.contains(urlPreview.url) {
if let previewPanelNode = currentPanel as? WebpagePreviewAccessoryPanelNode {
previewPanelNode.interfaceInteraction = interfaceInteraction
previewPanelNode.replaceWebpage(url: urlPreview.0, webpage: urlPreview.1)
previewPanelNode.replaceWebpage(url: urlPreview.url, webpage: urlPreview.webPage)
previewPanelNode.updateThemeAndStrings(theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
return previewPanelNode
} else {
let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.0, webpage: urlPreview.1, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
let panelNode = WebpagePreviewAccessoryPanelNode(context: context, url: urlPreview.url, webpage: urlPreview.webPage, theme: chatPresentationInterfaceState.theme, strings: chatPresentationInterfaceState.strings)
panelNode.interfaceInteraction = interfaceInteraction
return panelNode
}

View file

@ -382,7 +382,18 @@ func updatedChatEditInterfaceMessageState(state: ChatPresentationInterfaceState,
var updated = state
for media in message.media {
if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
updated = updated.updatedEditingUrlPreview((content.url, webpage))
let attribute = message.attributes.first(where: { $0 is WebpagePreviewMessageAttribute }) as? WebpagePreviewMessageAttribute
var positionBelowText = true
if let leadingPreview = attribute?.leadingPreview {
positionBelowText = !leadingPreview
}
let updatedPreview = ChatPresentationInterfaceState.UrlPreview(
url: content.url,
webPage: webpage,
positionBelowText: positionBelowText,
largeMedia: attribute?.forceLargeMedia
)
updated = updated.updatedEditingUrlPreview(updatedPreview)
}
}
var isPlaintext = true

View file

@ -477,27 +477,27 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre
private let dataDetector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType([.link]).rawValue)
func detectUrl(_ inputText: NSAttributedString?) -> String? {
var detectedUrl: String?
func detectUrls(_ inputText: NSAttributedString?) -> [String] {
var detectedUrls: [String] = []
if let text = inputText, let dataDetector = dataDetector {
let utf16 = text.string.utf16
let nsRange = NSRange(location: 0, length: utf16.count)
let matches = dataDetector.matches(in: text.string, options: [], range: nsRange)
if let match = matches.first {
for match in matches {
let urlText = (text.string as NSString).substring(with: match.range)
detectedUrl = urlText
detectedUrls.append(urlText)
}
if detectedUrl == nil {
inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in
if let value = value as? ChatTextInputTextUrlAttribute {
detectedUrl = value.url
inputText?.enumerateAttribute(ChatTextInputAttributes.textUrl, in: nsRange, options: [], using: { value, range, stop in
if let value = value as? ChatTextInputTextUrlAttribute {
if !detectedUrls.contains(value.url) {
detectedUrls.append(value.url)
}
})
}
}
})
}
return detectedUrl
return detectedUrls
}
func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: AccountContext, currentQuery: String?) -> (String?, Signal<(TelegramMediaWebpage?) -> TelegramMediaWebpage?, NoError>)? {
@ -509,7 +509,7 @@ func urlPreviewStateForInputText(_ inputText: NSAttributedString?, context: Acco
}
}
if let _ = dataDetector {
let detectedUrl = detectUrl(inputText)
let detectedUrl = detectUrls(inputText).first
if detectedUrl != currentQuery {
if let detectedUrl = detectedUrl {
return (detectedUrl, webpagePreview(account: context.account, url: detectedUrl) |> map { value in

View file

@ -858,7 +858,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if url.hasPrefix("tg://") {
isConcealed = false
}
controllerInteraction.openUrl(url, isConcealed, nil, nil)
controllerInteraction.openUrl(url, isConcealed, nil, nil, nil)
case .requestMap:
controllerInteraction.shareCurrentLocation()
case .requestPhone:

View file

@ -3665,14 +3665,27 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
return nil
}
var suggestedActionCounter: Int = 0
@available(iOS 13.0, *)
func chatInputTextNodeMenu(forTextRange textRange: NSRange, suggestedActions: [UIMenuElement]) -> UIMenu {
guard let editableTextNode = self.textInputNode else {
return UIMenu(children: [])
}
/*if "".isEmpty {
let index = self.suggestedActionCounter % suggestedActions.count
self.suggestedActionCounter += 1
print("action index: \(index)")
return UIMenu(children: [suggestedActions[index]])
}*/
var actions = suggestedActions
if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.replace;") }) {
actions.remove(at: index)
}
if editableTextNode.attributedText == nil || editableTextNode.attributedText!.length == 0 || editableTextNode.selectedRange.length == 0 {
} else {
@ -3732,7 +3745,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate, Ch
] as [UIAction])
let formatMenu = UIMenu(title: self.strings?.TextFormat_Format ?? "Format", image: nil, children: children)
actions.insert(formatMenu, at: 2)
if let index = actions.firstIndex(where: { $0.description.contains("identifier = com.apple.menu.format;") }) {
actions[index] = formatMenu
} else {
actions.insert(formatMenu, at: 2)
}
}
return UIMenu(children: actions)
}

View file

@ -17,6 +17,7 @@ import LegacyInstantVideoController
import StoryContainerScreen
import CameraScreen
import MediaEditorScreen
import ChatControllerInteraction
public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParams) {
if case let .peer(peer) = params.chatLocation, case let .channel(channel) = peer, channel.flags.contains(.isForum) {
@ -76,7 +77,7 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
if case let .id(messageId) = messageSubject {
let navigationController = params.navigationController
let animated = params.animated
controller.navigateToMessage(messageLocation: .id(messageId, timecode), animated: isFirst, completion: { [weak navigationController, weak controller] in
controller.navigateToMessage(messageLocation: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil)), animated: isFirst, completion: { [weak navigationController, weak controller] in
if let navigationController = navigationController, let controller = controller {
let _ = navigationController.popToViewController(controller, animated: animated)
}

View file

@ -81,7 +81,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, updateMessageReaction: { _, _ in
}, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in
}, navigateToMessage: { _, _ in
}, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: {
@ -98,7 +98,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { _, _, _, _ in
}, openUrl: { _, _, _, _, _ in
}, shareCurrentLocation: {
}, shareAccountContact: {
}, sendBotCommand: { _, _ in

View file

@ -367,7 +367,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
}
if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation {
if type == .music {
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(location: .id(id.messageId), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: MessageTags.music)
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tagMask: MessageTags.music)
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View file

@ -2729,7 +2729,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}
}
})
}, navigateToMessage: { fromId, id in
}, navigateToMessage: { _, _, _ in
}, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: nil, clickThroughMessage: {
@ -2766,7 +2766,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
}, requestMessageActionCallback: { _, _, _, _ in
}, requestMessageActionUrlAuth: { _, _ in
}, activateSwitchInline: { _, _, _ in
}, openUrl: { [weak self] url, concealed, external, _ in
}, openUrl: { [weak self] url, concealed, external, _, _ in
guard let strongSelf = self else {
return
}

View file

@ -135,7 +135,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
adjustedUpdateItems.append(ChatHistoryViewTransitionUpdateEntry(index: adjustedIndex, previousIndex: adjustedPreviousIndex, entry: entry, directionHint: directionHint))
}
var scrolledToIndex: MessageHistoryAnchorIndex?
var scrolledToIndex: MessageHistoryScrollToSubject?
var scrolledToSomeIndex = false
let curve: ListViewAnimationCurve = scrollAnimationCurve ?? .Default(duration: nil)
@ -193,13 +193,14 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
index += 1
}
}
case let .index(scrollIndex, position, directionHint, animated, highlight, displayLink):
case let .index(scrollSubject, position, directionHint, animated, highlight, displayLink):
let scrollIndex = scrollSubject
if case .center = position, highlight {
scrolledToIndex = scrollIndex
scrolledToIndex = scrollSubject
}
var index = toView.filteredEntries.count - 1
for entry in toView.filteredEntries {
if scrollIndex.isLessOrEqual(to: entry.index) {
if scrollIndex.index.isLessOrEqual(to: entry.index) {
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint, displayLink: displayLink)
break
}
@ -209,7 +210,7 @@ func preparedChatHistoryViewTransition(from fromView: ChatHistoryView?, to toVie
if scrollToItem == nil {
var index = 0
for entry in toView.filteredEntries.reversed() {
if !scrollIndex.isLess(than: entry.index) {
if !scrollIndex.index.isLess(than: entry.index) {
scrolledToSomeIndex = true
scrollToItem = ListViewScrollToItem(index: index, position: position, animated: animated, curve: curve, directionHint: directionHint)
break

View file

@ -1482,7 +1482,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
controllerInteraction = ChatControllerInteraction(openMessage: { _, _ in
return false }, openPeer: { _, _, _, _ in }, openPeerMention: { _ in }, openMessageContextMenu: { _, _, _, _, _, _ in }, openMessageReactionContextMenu: { _, _, _, _ in
}, updateMessageReaction: { _, _ in }, activateMessagePinch: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _ in }, navigateToMessageStandalone: { _ in
}, openMessageContextActions: { _, _, _, _ in }, navigateToMessage: { _, _, _ in }, navigateToMessageStandalone: { _ in
}, navigateToThreadMessage: { _, _, _ in
}, tapMessage: { message in
tapMessage?(message)
@ -1490,7 +1490,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
clickThroughMessage?()
}, toggleMessagesSelection: { _, _ in }, sendCurrentMessage: { _ in }, sendMessage: { _ in }, sendSticker: { _, _, _, _, _, _, _, _, _ in return false }, sendEmoji: { _, _, _ in }, sendGif: { _, _, _, _, _ in return false }, sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, requestMessageActionCallback: { _, _, _, _ in }, requestMessageActionUrlAuth: { _, _ in }, activateSwitchInline: { _, _, _ in }, openUrl: { _, _, _, _, _ in }, shareCurrentLocation: {}, shareAccountContact: {}, sendBotCommand: { _, _ in }, openInstantPage: { _, _ in }, openWallpaper: { _ in }, openTheme: { _ in }, openHashtag: { _, _ in }, updateInputState: { _ in }, updateInputMode: { _ in }, openMessageShareMenu: { _ in
}, presentController: { _, _ in
}, presentControllerInCurrent: { _, _ in
}, navigationController: {

View file

@ -210,7 +210,13 @@ public func stringWithAppliedEntities(_ text: String, entities: [MessageTextEnti
nsString = text as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
case .BlockQuote, .Code:
case .Code:
string.addAttribute(NSAttributedString.Key.font, value: fixedFont, range: range)
if nsString == nil {
nsString = text as NSString
}
string.addAttribute(NSAttributedString.Key(rawValue: TelegramTextAttributes.Pre), value: nsString!.substring(with: range), range: range)
case .BlockQuote:
if let fontAttribute = fontAttributes[range] {
fontAttributes[range] = fontAttribute.union(.blockQuote)
} else {

View file

@ -217,7 +217,9 @@ public func chatTranslationState(context: AccountContext, peerId: EnginePeer.Id)
ranges.append(text.index(text.startIndex, offsetBy: entity.range.lowerBound) ..< text.index(text.startIndex, offsetBy: entity.range.upperBound))
}
for range in ranges {
text.removeSubrange(range)
if range.upperBound < text.endIndex {
text.removeSubrange(range)
}
}
}