Merge branch 'master' into beta

This commit is contained in:
Ali 2020-10-07 21:14:02 +01:00
commit 8e41ec7d1b
89 changed files with 4145 additions and 3160 deletions

View File

@ -3,7 +3,7 @@
@implementation Serialization
- (NSUInteger)currentLayer {
return 119;
return 120;
}
- (id _Nullable)parseMessage:(NSData * _Nullable)data {

View File

@ -1921,6 +1921,7 @@
"Conversation.Unpin" = "Unpin";
"Conversation.Report" = "Report Spam";
"Conversation.PinnedMessage" = "Pinned Message";
"Conversation.PinnedPreviousMessage" = "Previous Message";
"Conversation.Moderate.Delete" = "Delete Message";
"Conversation.Moderate.Ban" = "Ban User";
@ -4098,7 +4099,7 @@ Unused sets are archived when you add more.";
"GroupPermission.PermissionGloballyDisabled" = "This permission is disabled in this group.";
"ChannelInfo.Stats" = "Statistics";
"ChannelInfo.Stats" = "View Statistics";
"Conversation.PressVolumeButtonForSound" = "Press volume button\nto unmute the video";
@ -5813,3 +5814,10 @@ Any member of this group will be able to see messages in the channel.";
"Channel.CommentsGroup.HeaderGroupSet" = "%@ is linking the group as it's discussion board.";
"RepliesChat.DescriptionText" = "This chat helps you keep track of replies to your comments in Channels.";
"Channel.DiscussionMessageUnavailable" = "Sorry, this post has been removed from the discussion group.";
"Conversation.ContextViewStats" = "View Statistics";
"ChatList.MessageMusic_1" = "1 Music File";
"ChatList.MessageMusic_any" = "%@ Music Files";

View File

@ -1 +1 @@
11.5
12.0.1

View File

@ -5,7 +5,7 @@ set -e
BUILD_TELEGRAM_VERSION="1"
MACOS_VERSION="10.15"
XCODE_VERSION="11.5"
XCODE_VERSION="12.0.1"
GUEST_SHELL="bash"
VM_BASE_NAME="macos$(echo $MACOS_VERSION | sed -e 's/\.'/_/g)_Xcode$(echo $XCODE_VERSION | sed -e 's/\.'/_/g)"

View File

@ -10,11 +10,6 @@ public class AnimatedCountLabelNode: ASDisplayNode {
}
public enum Segment: Equatable {
public enum Key: Hashable {
case number
case text(Int)
}
case number(Int, NSAttributedString)
case text(Int, NSAttributedString)
@ -34,10 +29,37 @@ public class AnimatedCountLabelNode: ASDisplayNode {
}
}
}
}
fileprivate enum ResolvedSegment: Equatable {
public enum Key: Hashable {
case number(Int)
case text(Int)
}
case number(id: Int, value: Int, string: NSAttributedString)
case text(id: Int, string: NSAttributedString)
public static func ==(lhs: ResolvedSegment, rhs: ResolvedSegment) -> Bool {
switch lhs {
case let .number(id, number, text):
if case let .number(rhsId, rhsNumber, rhsText) = rhs, id == rhsId, number == rhsNumber, text.isEqual(to: rhsText) {
return true
} else {
return false
}
case let .text(index, text):
if case let .text(rhsIndex, rhsText) = rhs, index == rhsIndex, text.isEqual(to: rhsText) {
return true
} else {
return false
}
}
}
public var attributedText: NSAttributedString {
switch self {
case let .number(_, text):
case let .number(_, _, text):
return text
case let .text(_, text):
return text
@ -46,28 +68,53 @@ public class AnimatedCountLabelNode: ASDisplayNode {
var key: Key {
switch self {
case .number:
return .number
case let .number(id, _, _):
return .number(id)
case let .text(index, _):
return .text(index)
}
}
}
fileprivate var resolvedSegments: [Segment.Key: (Segment, TextNode)] = [:]
fileprivate var resolvedSegments: [ResolvedSegment.Key: (ResolvedSegment, TextNode)] = [:]
override public init() {
super.init()
}
public func asyncLayout() -> (CGSize, [Segment]) -> (Layout, (Bool) -> Void) {
var segmentLayouts: [Segment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
var segmentLayouts: [ResolvedSegment.Key: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = [:]
let wasEmpty = self.resolvedSegments.isEmpty
for (segmentKey, segmentAndTextNode) in self.resolvedSegments {
segmentLayouts[segmentKey] = TextNode.asyncLayout(segmentAndTextNode.1)
}
return { [weak self] size, segments in
return { [weak self] size, initialSegments in
var segments: [ResolvedSegment] = []
loop: for segment in initialSegments {
switch segment {
case let .number(value, string):
if string.string.isEmpty {
continue loop
}
let attributes = string.attributes(at: 0, longestEffectiveRange: nil, in: NSRange(location: 0, length: 1))
var remainingValue = value
while true {
let digitValue = remainingValue % 10
segments.insert(.number(id: 1000 - segments.count, value: value, string: NSAttributedString(string: "\(digitValue)", attributes: attributes)), at: 0)
remainingValue /= 10
if remainingValue == 0 {
break
}
}
case let .text(id, string):
segments.append(.text(id: id, string: string))
}
}
for segment in segments {
if segmentLayouts[segment.key] == nil {
segmentLayouts[segment.key] = TextNode.asyncLayout(nil)
@ -77,17 +124,17 @@ public class AnimatedCountLabelNode: ASDisplayNode {
var contentSize = CGSize()
var remainingSize = size
var calculatedSegments: [Segment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:]
var calculatedSegments: [ResolvedSegment.Key: (TextNodeLayout, CGFloat, () -> TextNode)] = [:]
var isTruncated = false
var validKeys: [Segment.Key] = []
var validKeys: [ResolvedSegment.Key] = []
for segment in segments {
validKeys.append(segment.key)
let (layout, apply) = segmentLayouts[segment.key]!(TextNodeLayoutArguments(attributedString: segment.attributedText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: remainingSize, alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets(), lineColor: nil, textShadowColor: nil, textStroke: nil))
var effectiveSegmentWidth = layout.size.width
if case .number = segment {
effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0
//effectiveSegmentWidth = ceil(effectiveSegmentWidth / 2.0) * 2.0
} else if segment.attributedText.string == " " {
effectiveSegmentWidth = max(effectiveSegmentWidth, 4.0)
}
@ -115,7 +162,7 @@ public class AnimatedCountLabelNode: ASDisplayNode {
for segment in segments {
var animation: (CGFloat, Double)?
if let (currentSegment, currentTextNode) = strongSelf.resolvedSegments[segment.key] {
if case let .number(currentValue, _) = currentSegment, case let .number(updatedValue, _) = segment, animated, !wasEmpty, currentValue != updatedValue, let snapshot = currentTextNode.layer.snapshotContentTree() {
if case let .number(_, currentValue, currentString) = currentSegment, case let .number(_, updatedValue, updatedString) = segment, animated, !wasEmpty, currentValue != updatedValue, currentString.string != updatedString.string, let snapshot = currentTextNode.layer.snapshotContentTree() {
let offsetY: CGFloat
if currentValue > updatedValue {
offsetY = -floor(currentTextNode.bounds.height * 0.6)
@ -164,7 +211,7 @@ public class AnimatedCountLabelNode: ASDisplayNode {
strongSelf.resolvedSegments[segment.key] = (segment, textNode)
}
var removeKeys: [Segment.Key] = []
var removeKeys: [ResolvedSegment.Key] = []
for key in strongSelf.resolvedSegments.keys {
if !validKeys.contains(key) {
removeKeys.append(key)

View File

@ -37,8 +37,6 @@ private func isLocked(passcodeSettings: PresentationPasscodeSettings, state: Loc
}
private func getCoveringViewSnaphot(window: Window1) -> UIImage? {
print("getCoveringViewSnaphot")
let scale: CGFloat = 0.5
let unscaledSize = window.hostView.containerView.frame.size
return generateImage(CGSize(width: floor(unscaledSize.width * scale), height: floor(unscaledSize.height * scale)), rotatedContext: { size, context in

View File

@ -1494,6 +1494,18 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
if let strongSelf = self {
strongSelf.presentationData = presentationData
strongSelf.presentationDataPromise.set(.single(ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: presentationData.disableAnimations)))
strongSelf.listNode.forEachItemHeaderNode({ itemHeaderNode in
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
itemHeaderNode.updateTheme(theme: presentationData.theme)
}
})
strongSelf.recentListNode.forEachItemHeaderNode({ itemHeaderNode in
if let itemHeaderNode = itemHeaderNode as? ChatListSearchItemHeaderNode {
itemHeaderNode.updateTheme(theme: presentationData.theme)
}
})
}
})

View File

@ -104,13 +104,13 @@ func suggestDates(for string: String, strings: PresentationStrings, dateTimeForm
let stringComponents = string.components(separatedBy: dateSeparator)
if stringComponents.count < 3 {
for i in 0..<5 {
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now {
for i in 0..<8 {
if let date = calendar.date(byAdding: .year, value: -i, to: resultDate), date < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, date, nil))
}
}
} else if resultDate < now {
} else if resultDate < now, date > telegramReleaseDate {
let lowerDate = getLowerDate(for: resultDate)
result.append((lowerDate, resultDate, nil))
}

View File

@ -10,6 +10,7 @@ import LocalizedPeerData
private enum MessageGroupType {
case photos
case videos
case music
case generic
}
@ -18,6 +19,9 @@ private func singleMessageType(message: Message) -> MessageGroupType {
if let _ = media as? TelegramMediaImage {
return .photos
} else if let file = media as? TelegramMediaFile {
if file.isMusic {
return .music
}
if file.isVideo && !file.isInstantVideo {
return .videos
}
@ -80,6 +84,13 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
messageText = strings.ChatList_MessageVideos(Int32(messages.count))
textIsReady = true
}
case .music:
if !messageText.isEmpty {
textIsReady = true
} else {
messageText = strings.ChatList_MessageMusic(Int32(messages.count))
textIsReady = true
}
case .generic:
break
}

View File

@ -8,6 +8,7 @@ public enum CheckNodeStyle {
case plain
case overlay
case navigation
case compact
}
public final class CheckNode: ASDisplayNode {
@ -47,6 +48,9 @@ public final class CheckNode: ASDisplayNode {
case .navigation:
style = TGCheckButtonStyleGallery
checkSize = CGSize(width: 39.0, height: 39.0)
case .compact:
style = TGCheckButtonStyleCompact
checkSize = CGSize(width: 30.0, height: 30.0)
}
let checkView = TGCheckButtonView(style: style, pallete: TGCheckButtonPallete(defaultBackgroundColor: self.fillColor, accentBackgroundColor: self.fillColor, defaultBorderColor: self.strokeColor, mediaBorderColor: self.strokeColor, chatBorderColor: self.strokeColor, check: self.foregroundColor, blueColor: self.fillColor, barBackgroundColor: self.fillColor))!
checkView.setSelected(true, animated: false)

View File

@ -210,7 +210,9 @@ public func galleryItemForEntry(context: AccountContext, presentationData: Prese
content = SystemVideoContent(url: embedUrl, imageReference: .webPage(webPage: WebpageReference(webpage), media: image), dimensions: webpageContent.embedSize?.cgSize ?? CGSize(width: 640.0, height: 640.0), duration: Int32(webpageContent.duration ?? 0))
}
}
if content == nil, let webEmbedContent = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init)) {
if content == nil, let webEmbedContent = WebEmbedVideoContent(webPage: webpage, webpageContent: webpageContent, forcedTimestamp: timecode.flatMap(Int.init), openUrl: { url in
performAction(.url(url: url.absoluteString, concealed: false))
}) {
content = webEmbedContent
}
}

View File

@ -493,6 +493,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
var disablePictureInPicture = false
var disablePlayerControls = false
var forceEnablePiP = false
var forceEnableUserInteraction = false
var isAnimated = false
if let content = item.content as? NativeVideoContent {
isAnimated = content.fileReference.media.isAnimated
@ -503,6 +504,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let type = webEmbedType(content: content.webpageContent)
switch type {
case .youtube:
forceEnableUserInteraction = true
disablePictureInPicture = !(item.configuration?.youtubePictureInPictureEnabled ?? false)
self.videoFramePreview = YoutubeEmbedFramePreview(context: item.context, content: content)
case .iframe:
@ -527,7 +529,14 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
let mediaManager = item.context.sharedContext.mediaManager
let videoNode = UniversalVideoNode(postbox: item.context.account.postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: item.content, priority: .gallery)
let videoSize = CGSize(width: item.content.dimensions.width * 2.0, height: item.content.dimensions.height * 2.0)
let videoScale: CGFloat
if item.content is WebEmbedVideoContent {
videoScale = 1.0
} else {
videoScale = 2.0
}
let videoSize = CGSize(width: item.content.dimensions.width * videoScale, height: item.content.dimensions.height * videoScale)
videoNode.updateLayout(size: videoSize, transition: .immediate)
videoNode.ownsContentNodeUpdated = { [weak self] value in
if let strongSelf = self {
@ -546,7 +555,7 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
}
}
self.videoNode = videoNode
videoNode.isUserInteractionEnabled = disablePlayerControls
videoNode.isUserInteractionEnabled = disablePlayerControls || forceEnableUserInteraction
videoNode.backgroundColor = videoNode.ownsContentNode ? UIColor.black : UIColor(rgb: 0x333335)
if item.fromPlayingVideo {
videoNode.canAttachContent = false

View File

@ -44,6 +44,15 @@ public class BaseLinesChartController: BaseChartController {
self.setBackButtonVisibilityClosure?(isZoomed, animated)
updateChartRangeTitle(animated: animated)
let initial = initialChartsCollection
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
if let lastDate = initial.axisValues.last {
TimeInterval.animationDurationMultipler = 0.00001
self.didTapZoomIn(date: lastDate, pointIndex: initial.axisValues.count - 1)
TimeInterval.animationDurationMultipler = 1.0
}
}
}
func updateChartRangeTitle(animated: Bool) {

View File

@ -137,7 +137,9 @@ public struct InstantPageGalleryEntry: Equatable {
}))
}), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
} else {
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent) {
if let content = WebEmbedVideoContent(webPage: embedWebpage, webpageContent: webpageContent, openUrl: { url in
}) {
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, nil), caption: NSAttributedString(string: ""), fromPlayingVideo: fromPlayingVideo, landscape: landscape, performAction: { _ in }, openActionOptions: { _ in }, storeMediaPlaybackState: { _, _ in }, present: { _, _ in })
} else {
preconditionFailure()

View File

@ -8,7 +8,8 @@ typedef enum
TGCheckButtonStyleMedia,
TGCheckButtonStyleGallery,
TGCheckButtonStyleShare,
TGCheckButtonStyleChat
TGCheckButtonStyleChat,
TGCheckButtonStyleCompact
} TGCheckButtonStyle;
@interface TGCheckButtonPallete : NSObject

View File

@ -106,6 +106,12 @@
}
break;
case TGCheckButtonStyleCompact:
{
insideInset = 6.0f;
}
break;
default:
{
insideInset = 5.0f;
@ -182,13 +188,18 @@
default:
{
CGFloat lineWidth = 1.0f;
if (style == TGCheckButtonStyleCompact) {
lineWidth = 1.5f;
}
CGRect rect = CGRectMake(0, 0, size.width, size.height);
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.0f);
CGContextSetLineWidth(context, lineWidth);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + 0.5f, insideInset + 0.5f));
CGContextStrokeEllipseInRect(context, CGRectInset(rect, insideInset + lineWidth / 2.0, insideInset + lineWidth / 2.0));
backgroundImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
@ -234,7 +245,7 @@
UIColor *color = style == TGCheckButtonStyleDefaultBlue ? blueColor : greenColor;
CGContextSetFillColorWithColor(context, color.CGColor);
CGFloat inset = (style == TGCheckButtonStyleDefault || style == TGCheckButtonStyleDefaultBlue) ? 0.0f : 1.2f;
CGFloat inset = (style == TGCheckButtonStyleDefault || style == TGCheckButtonStyleDefaultBlue || style == TGCheckButtonStyleCompact) ? 0.0f : 1.2f;
CGContextFillEllipseInRect(context, CGRectInset(rect, insideInset + inset, insideInset + inset));
fillImage = UIGraphicsGetImageFromCurrentImageContext();

View File

@ -14,7 +14,7 @@ public func legacySuggestionContext(context: AccountContext, peerId: PeerId, cha
suggestionContext.userListSignal = { query in
return SSignal { subscriber in
if let query = query {
let disposable = searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query).start(next: { peers in
let disposable = searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query, scope: .mention).start(next: { peers in
let users = NSMutableArray()
for peer in peers {
let user = TGUser()

View File

@ -380,12 +380,12 @@ static int32_t fixedTimeDifferenceValue = 0;
if (datacenterAuthInfoById != nil) {
_datacenterAuthInfoById = [[NSMutableDictionary alloc] initWithDictionary:datacenterAuthInfoById];
#if DEBUG
/*NSArray<NSNumber *> *keys = [_datacenterAuthInfoById allKeys];
NSArray<NSNumber *> *keys = [_datacenterAuthInfoById allKeys];
for (NSNumber *key in keys) {
if (parseAuthInfoMapKeyInteger(key).selector != MTDatacenterAuthInfoSelectorPersistent) {
[_datacenterAuthInfoById removeObjectForKey:key];
}
}*/
}
#endif
}

View File

@ -417,6 +417,9 @@ typedef enum {
[newEncryptedData appendData:encryptedData];
encryptedData = newEncryptedData;
}
#if DEBUG
assert(encryptedData.length == 256);
#endif
_dhEncryptedData = encryptedData;
} else {

View File

@ -560,9 +560,15 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
var isEmptyState = false
var displayGroupList = false
if let cachedData = view.cachedData as? CachedChannelData {
let isEmpty = cachedData.linkedDiscussionPeerId == nil
var isEmpty = true
switch cachedData.linkedDiscussionPeerId {
case .unknown:
isEmpty = true
case let .known(value):
isEmpty = value == nil
}
if let peer = view.peers[view.peerId] as? TelegramChannel, case .broadcast = peer.info {
if cachedData.linkedDiscussionPeerId == nil {
if isEmpty {
if groups == nil {
isEmptyState = true
} else {
@ -570,13 +576,13 @@ public func channelDiscussionGroupSetupController(context: AccountContext, peerI
}
}
}
if let wasEmpty = wasEmpty, wasEmpty != isEmpty {
crossfade = true
}
wasEmpty = isEmpty
} else {
isEmptyState = true
}
if let wasEmpty = wasEmpty, wasEmpty != isEmptyState {
crossfade = true
}
wasEmpty = isEmptyState
var emptyStateItem: ItemListControllerEmptyStateItem?
if isEmptyState {

View File

@ -1105,7 +1105,7 @@ func debugRestoreState(basePath:String, name: String) {
private let sharedQueue = Queue(name: "org.telegram.postbox.Postbox")
public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<PostboxResult, NoError> {
public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration, encryptionParameters: ValueBoxEncryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32) -> Signal<PostboxResult, NoError> {
let queue = sharedQueue
return Signal { subscriber in
queue.async {
@ -1177,7 +1177,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration,
let endTime = CFAbsoluteTimeGetCurrent()
print("Postbox load took \((endTime - startTime) * 1000.0) ms")
subscriber.putNext(.postbox(Postbox(queue: queue, basePath: basePath, seedConfiguration: seedConfiguration, valueBox: valueBox)))
subscriber.putNext(.postbox(Postbox(queue: queue, basePath: basePath, seedConfiguration: seedConfiguration, valueBox: valueBox, timestampForAbsoluteTimeBasedOperations: timestampForAbsoluteTimeBasedOperations)))
subscriber.putCompletion()
break
}
@ -1330,7 +1330,7 @@ public final class Postbox {
var installedMessageActionsByPeerId: [PeerId: Bag<([StoreMessage], Transaction) -> Void>] = [:]
init(queue: Queue, basePath: String, seedConfiguration: SeedConfiguration, valueBox: SqliteValueBox) {
init(queue: Queue, basePath: String, seedConfiguration: SeedConfiguration, valueBox: SqliteValueBox, timestampForAbsoluteTimeBasedOperations: Int32) {
assert(queue.isCurrent())
let startTime = CFAbsoluteTimeGetCurrent()
@ -1534,6 +1534,9 @@ public final class Postbox {
for id in self.messageHistoryUnsentTable.get() {
transaction.updateMessage(id, update: { message in
if !message.flags.contains(.Failed) {
if message.timestamp + 60 * 10 > timestampForAbsoluteTimeBasedOperations {
return .skip
}
var flags = StoreMessageFlags(message.flags)
flags.remove(.Unsent)
flags.remove(.Sending)

View File

@ -5,20 +5,23 @@ import SyncCore
import SwiftSignalKit
import AccountContext
public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, query: String) -> Signal<[Peer], NoError> {
if case .replyThread = chatLocation {
return .single([])
} else if peerId.namespace == Namespaces.Peer.CloudChannel {
public enum SearchPeerMembersScope {
case memberSuggestion
case mention
}
public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocation: ChatLocation, query: String, scope: SearchPeerMembersScope) -> Signal<[Peer], NoError> {
if peerId.namespace == Namespaces.Peer.CloudChannel {
return context.account.postbox.transaction { transaction -> CachedChannelData? in
return transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData
}
|> mapToSignal { cachedData -> Signal<[Peer], NoError> in
if let cachedData = cachedData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 64 {
|> mapToSignal { cachedData -> Signal<([Peer], Bool), NoError> in
if case .peer = chatLocation, let cachedData = cachedData, let memberCount = cachedData.participantsSummary.memberCount, memberCount <= 64 {
return Signal { subscriber in
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: nil, requestUpdate: false, updated: { state in
if case .ready = state.loadingState {
let normalizedQuery = query.lowercased()
subscriber.putNext(state.list.compactMap { participant -> Peer? in
subscriber.putNext((state.list.compactMap { participant -> Peer? in
if participant.peer.isDeleted {
return nil
}
@ -37,7 +40,7 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat
return nil
}
})
}, true))
}
})
@ -49,22 +52,70 @@ public func searchPeerMembers(context: AccountContext, peerId: PeerId, chatLocat
}
return Signal { subscriber in
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query.isEmpty ? nil : query, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext(state.list.compactMap { participant in
if participant.peer.isDeleted {
return nil
}
return participant.peer
})
switch chatLocation {
case let .peer(peerId):
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, searchQuery: query.isEmpty ? nil : query, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext((state.list.compactMap { participant in
if participant.peer.isDeleted {
return nil
}
return participant.peer
}, true))
}
})
return ActionDisposable {
disposable.dispose()
}
case let .replyThread(replyThreadMessage):
let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.mentions(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, threadMessageId: replyThreadMessage.messageId, searchQuery: query.isEmpty ? nil : query, updated: { state in
if case .ready = state.loadingState {
subscriber.putNext((state.list.compactMap { participant in
if participant.peer.isDeleted {
return nil
}
return participant.peer
}, true))
}
})
return ActionDisposable {
disposable.dispose()
}
})
return ActionDisposable {
disposable.dispose()
}
} |> runOn(Queue.mainQueue())
}
|> mapToSignal { result, isReady -> Signal<[Peer], NoError> in
switch scope {
case .mention:
return .single(result)
case .memberSuggestion:
return context.account.postbox.transaction { transaction -> [Peer] in
var result = result
let normalizedQuery = query.lowercased()
if isReady {
if let channel = transaction.getPeer(peerId) as? TelegramChannel, case .group = channel.info {
var matches = false
if normalizedQuery.isEmpty {
matches = true
} else {
if channel.indexName.matchesByTokens(normalizedQuery) {
matches = true
}
if let addressName = channel.addressName, addressName.lowercased().hasPrefix(normalizedQuery) {
matches = true
}
}
if matches {
result.insert(channel, at: 0)
}
}
}
return result
}
}
}
} else {
return searchGroupMembers(postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peerId, query: query)
}

View File

@ -454,13 +454,15 @@ private final class SemanticStatusNodeDrawingState: NSObject {
let hollow: Bool
let transitionState: SemanticStatusNodeTransitionDrawingState?
let drawingState: SemanticStatusNodeStateDrawingState
let cutout: SemanticStatusNode.Cutout?
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState) {
init(background: UIColor, foreground: UIColor, hollow: Bool, transitionState: SemanticStatusNodeTransitionDrawingState?, drawingState: SemanticStatusNodeStateDrawingState, cutout: SemanticStatusNode.Cutout?) {
self.background = background
self.foreground = foreground
self.hollow = hollow
self.transitionState = transitionState
self.drawingState = drawingState
self.cutout = cutout
super.init()
}
@ -481,6 +483,10 @@ private final class SemanticStatusNodeTransitionContext {
}
public final class SemanticStatusNode: ASControlNode {
final class Cutout {
}
public var backgroundNodeColor: UIColor {
didSet {
if !self.backgroundNodeColor.isEqual(oldValue) {
@ -589,7 +595,7 @@ public final class SemanticStatusNode: ASControlNode {
transitionState = SemanticStatusNodeTransitionDrawingState(transition: t, drawingState: transitionContext.previousStateContext.drawingState(transitionFraction: 1.0 - t))
}
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction))
return SemanticStatusNodeDrawingState(background: self.backgroundNodeColor, foreground: self.foregroundNodeColor, hollow: self.hollow, transitionState: transitionState, drawingState: self.stateContext.drawingState(transitionFraction: transitionFraction), cutout: nil)
}
@objc override public class func draw(_ bounds: CGRect, withParameters parameters: Any?, isCancelled: () -> Bool, isRasterizing: Bool) {

View File

@ -39,7 +39,7 @@ private enum StatsEntry: ItemListNodeEntry {
case overview(PresentationTheme, MessageStats, Int32?)
case interactionsTitle(PresentationTheme, String)
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, ChartType)
case interactionsGraph(PresentationTheme, PresentationStrings, PresentationDateTimeFormat, StatsGraph, StatsGraph?, ChartType)
case publicForwardsTitle(PresentationTheme, String)
case publicForward(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, Message)
@ -92,8 +92,8 @@ private enum StatsEntry: ItemListNodeEntry {
} else {
return false
}
case let .interactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsType):
if case let .interactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsType == rhsType {
case let .interactionsGraph(lhsTheme, lhsStrings, lhsDateTimeFormat, lhsGraph, lhsDetailedGraph, lhsType):
if case let .interactionsGraph(rhsTheme, rhsStrings, rhsDateTimeFormat, rhsGraph, rhsDetailedGraph, rhsType) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsDateTimeFormat == rhsDateTimeFormat, lhsGraph == rhsGraph, lhsDetailedGraph == rhsDetailedGraph, lhsType == rhsType {
return true
} else {
return false
@ -126,13 +126,17 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
case let .overview(_, stats, publicShares):
return MessageStatsOverviewItem(presentationData: presentationData, stats: stats, publicShares: publicShares, sectionId: self.section, style: .blocks)
case let .interactionsGraph(_, _, _, graph, type):
case let .interactionsGraph(_, _, _, graph, detailedGraph, type):
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph {
completion(data)
}
})
if let detailedGraph = detailedGraph, case let .Loaded(_, data) = detailedGraph {
completion(data)
} else {
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
if let graph = graph, case let .Loaded(_, data) = graph {
completion(data)
}
})
}
}, sectionId: self.section, style: .blocks)
case let .publicForward(_, _, _, _, message):
var views: Int = 0
@ -148,9 +152,6 @@ private enum StatsEntry: ItemListNodeEntry {
return ItemListPeerItem(presentationData: presentationData, dateTimeFormat: PresentationDateTimeFormat(timeFormat: .military, dateFormat: .dayFirst, dateSeparator: ".", decimalSeparator: ",", groupingSeparator: ""), nameDisplayOrder: .firstLast, context: arguments.context, peer: message.peers[message.id.peerId]!, height: .generic, aliasHandling: .standard, nameColor: .primary, nameStyle: .plain, presence: nil, text: .text(text), label: .none, editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: nil), revealOptions: nil, switchValue: nil, enabled: true, highlighted: false, selectable: true, sectionId: self.section, action: {
arguments.openMessage(message.id)
}, setPeerIdWithRevealedOptions: { _, _ in }, removePeer: { _ in }, toggleUpdated: nil, contextAction: nil)
// return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: 0, forwards: 0, sectionId: self.section, style: .blocks, action: {
// arguments.openMessage(message.id)
// })
}
}
}
@ -164,7 +165,7 @@ private func messageStatsControllerEntries(data: MessageStats?, messages: Search
if !data.interactionsGraph.isEmpty {
entries.append(.interactionsTitle(presentationData.theme, presentationData.strings.Stats_MessageInteractionsTitle.uppercased()))
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
entries.append(.interactionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, data.detailedInteractionsGraph, .twoAxisStep))
}
if let messages = messages, !messages.messages.isEmpty {

View File

@ -1,3 +1,4 @@
import Foundation
import SwiftSignalKit
import Postbox
@ -67,7 +68,7 @@ public let telegramPostboxSeedConfiguration: SeedConfiguration = {
public func accountTransaction<T>(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, transaction: @escaping (Transaction) -> T) -> Signal<T, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
return postbox
|> mapToSignal { value -> Signal<T, NoError> in
switch value {

View File

@ -322,6 +322,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[106343499] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsSearch($0) }
dict[-1548400251] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsKicked($0) }
dict[-1150621555] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsContacts($0) }
dict[-531931925] = { return Api.ChannelParticipantsFilter.parse_channelParticipantsMentions($0) }
dict[-350980120] = { return Api.WebPage.parse_webPageEmpty($0) }
dict[-981018084] = { return Api.WebPage.parse_webPagePending($0) }
dict[-392411726] = { return Api.WebPage.parse_webPage($0) }

View File

@ -9991,6 +9991,7 @@ public extension Api {
case channelParticipantsSearch(q: String)
case channelParticipantsKicked(q: String)
case channelParticipantsContacts(q: String)
case channelParticipantsMentions(flags: Int32, q: String?, topMsgId: Int32?)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self {
@ -10036,6 +10037,14 @@ public extension Api {
}
serializeString(q, buffer: buffer, boxed: false)
break
case .channelParticipantsMentions(let flags, let q, let topMsgId):
if boxed {
buffer.appendInt32(-531931925)
}
serializeInt32(flags, buffer: buffer, boxed: false)
if Int(flags) & Int(1 << 0) != 0 {serializeString(q!, buffer: buffer, boxed: false)}
if Int(flags) & Int(1 << 1) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
break
}
}
@ -10055,6 +10064,8 @@ public extension Api {
return ("channelParticipantsKicked", [("q", q)])
case .channelParticipantsContacts(let q):
return ("channelParticipantsContacts", [("q", q)])
case .channelParticipantsMentions(let flags, let q, let topMsgId):
return ("channelParticipantsMentions", [("flags", flags), ("q", q), ("topMsgId", topMsgId)])
}
}
@ -10111,6 +10122,23 @@ public extension Api {
return nil
}
}
public static func parse_channelParticipantsMentions(_ reader: BufferReader) -> ChannelParticipantsFilter? {
var _1: Int32?
_1 = reader.readInt32()
var _2: String?
if Int(_1!) & Int(1 << 0) != 0 {_2 = parseString(reader) }
var _3: Int32?
if Int(_1!) & Int(1 << 1) != 0 {_3 = reader.readInt32() }
let _c1 = _1 != nil
let _c2 = (Int(_1!) & Int(1 << 0) == 0) || _2 != nil
let _c3 = (Int(_1!) & Int(1 << 1) == 0) || _3 != nil
if _c1 && _c2 && _c3 {
return Api.ChannelParticipantsFilter.channelParticipantsMentions(flags: _1!, q: _2, topMsgId: _3)
}
else {
return nil
}
}
}
public enum WebPage: TypeConstructorDescription {

View File

@ -3737,9 +3737,26 @@ public extension Api {
})
}
public static func search(flags: Int32, peer: Api.InputPeer, q: String, fromId: Api.InputUser?, topMsgId: Int32?, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetId: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
public static func setTyping(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?, action: Api.SendMessageAction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1310163211)
buffer.appendInt32(1486110434)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
action.serialize(buffer, true)
return (FunctionDescription(name: "messages.setTyping", parameters: [("flags", flags), ("peer", peer), ("topMsgId", topMsgId), ("action", action)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
public static func search(flags: Int32, peer: Api.InputPeer, q: String, fromId: Api.InputPeer?, topMsgId: Int32?, filter: Api.MessagesFilter, minDate: Int32, maxDate: Int32, offsetId: Int32, addOffset: Int32, limit: Int32, maxId: Int32, minId: Int32, hash: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.Messages>) {
let buffer = Buffer()
buffer.appendInt32(204812012)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
serializeString(q, buffer: buffer, boxed: false)
@ -3763,23 +3780,6 @@ public extension Api {
return result
})
}
public static func setTyping(flags: Int32, peer: Api.InputPeer, topMsgId: Int32?, action: Api.SendMessageAction) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {
let buffer = Buffer()
buffer.appendInt32(1486110434)
serializeInt32(flags, buffer: buffer, boxed: false)
peer.serialize(buffer, true)
if Int(flags) & Int(1 << 0) != 0 {serializeInt32(topMsgId!, buffer: buffer, boxed: false)}
action.serialize(buffer, true)
return (FunctionDescription(name: "messages.setTyping", parameters: [("flags", flags), ("peer", peer), ("topMsgId", topMsgId), ("action", action)]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in
let reader = BufferReader(buffer)
var result: Api.Bool?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Bool
}
return result
})
}
}
public struct channels {
public static func readHistory(channel: Api.InputChannel, maxId: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Bool>) {

View File

@ -160,7 +160,7 @@ public enum AccountPreferenceEntriesResult {
public func accountPreferenceEntries(rootPath: String, id: AccountRecordId, keys: Set<ValueBoxKey>, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<AccountPreferenceEntriesResult, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
return postbox
|> mapToSignal { value -> Signal<AccountPreferenceEntriesResult, NoError> in
switch value {
@ -187,7 +187,7 @@ public enum AccountNoticeEntriesResult {
public func accountNoticeEntries(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<AccountNoticeEntriesResult, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
return postbox
|> mapToSignal { value -> Signal<AccountNoticeEntriesResult, NoError> in
switch value {
@ -208,7 +208,7 @@ public enum LegacyAccessChallengeDataResult {
public func accountLegacyAccessChallengeData(rootPath: String, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters) -> Signal<LegacyAccessChallengeDataResult, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
return postbox
|> mapToSignal { value -> Signal<LegacyAccessChallengeDataResult, NoError> in
switch value {
@ -225,7 +225,7 @@ public func accountLegacyAccessChallengeData(rootPath: String, id: AccountRecord
public func accountWithId(accountManager: AccountManager, networkArguments: NetworkInitializationArguments, id: AccountRecordId, encryptionParameters: ValueBoxEncryptionParameters, supplementary: Bool, rootPath: String, beginWithTestingEnvironment: Bool, backupData: AccountBackupData?, auxiliaryMethods: AccountAuxiliaryMethods, shouldKeepAutoConnection: Bool = true) -> Signal<AccountResult, NoError> {
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
let postbox = openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
return postbox
|> mapToSignal { result -> Signal<AccountResult, NoError> in

View File

@ -209,7 +209,7 @@ public func temporaryAccount(manager: AccountManager, rootPath: String, encrypti
return manager.allocatedTemporaryAccountId()
|> mapToSignal { id -> Signal<TemporaryAccount, NoError> in
let path = "\(rootPath)/\(accountRecordIdPathName(id))"
return openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters)
return openPostbox(basePath: path + "/postbox", seedConfiguration: telegramPostboxSeedConfiguration, encryptionParameters: encryptionParameters, timestampForAbsoluteTimeBasedOperations: Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970))
|> mapToSignal { result -> Signal<TemporaryAccount, NoError> in
switch result {
case .upgrading:

View File

@ -291,6 +291,7 @@ public final class AccountViewTracker {
private var seenLiveLocationDisposables = DisposableDict<Int32>()
private var updatedUnsupportedMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:]
private var refreshSecretChatMediaMessageIdsAndTimestamps: [MessageId: Int32] = [:]
private var nextUpdatedUnsupportedMediaDisposableId: Int32 = 0
private var updatedUnsupportedMediaDisposables = DisposableDict<Int32>()
@ -631,6 +632,18 @@ public final class AccountViewTracker {
|> runOn(self.queue)
}
public func updateReplyInfoForMessageId(_ id: MessageId, info: UpdatedMessageReplyInfo) {
self.queue.async { [weak self] in
guard let strongSelf = self else {
return
}
guard let current = strongSelf.updatedViewCountMessageIdsAndTimestamps[id] else {
return
}
strongSelf.updatedViewCountMessageIdsAndTimestamps[id] = ViewCountContextState(timestamp: Int32(CFAbsoluteTimeGetCurrent()), clientId: current.clientId, result: ViewCountContextState.ReplyInfo(commentsPeerId: info.commentsPeerId, maxReadIncomingMessageId: info.maxReadIncomingMessageId, maxMessageId: info.maxMessageId))
}
}
public func updateViewCountForMessageIds(messageIds: Set<MessageId>, clientId: Int32) {
self.queue.async {
var addedMessageIds: [MessageId] = []
@ -1037,6 +1050,98 @@ public final class AccountViewTracker {
}
}
public func refreshSecretMediaMediaForMessageIds(messageIds: Set<MessageId>) {
self.queue.async {
var addedMessageIds: [MessageId] = []
let timestamp = Int32(CFAbsoluteTimeGetCurrent())
for messageId in messageIds {
let messageTimestamp = self.refreshSecretChatMediaMessageIdsAndTimestamps[messageId]
if messageTimestamp == nil {
self.refreshSecretChatMediaMessageIdsAndTimestamps[messageId] = timestamp
addedMessageIds.append(messageId)
}
}
if !addedMessageIds.isEmpty {
for (_, messageIds) in messagesIdsGroupedByPeerId(Set(addedMessageIds)) {
let disposableId = self.nextUpdatedUnsupportedMediaDisposableId
self.nextUpdatedUnsupportedMediaDisposableId += 1
if let account = self.account {
let signal = account.postbox.transaction { transaction -> [TelegramMediaFile] in
var result: [TelegramMediaFile] = []
for id in messageIds {
if let message = transaction.getMessage(id) {
for media in message.media {
if let file = media as? TelegramMediaFile, file.isAnimatedSticker {
result.append(file)
}
}
}
}
return result
}
|> mapToSignal { files -> Signal<Void, NoError> in
guard !files.isEmpty else {
return .complete()
}
var stickerPacks = Set<StickerPackReference>()
for file in files {
for attribute in file.attributes {
if case let .Sticker(_, packReferenceValue, _) = attribute, let packReference = packReferenceValue {
if case .id = packReference {
stickerPacks.insert(packReference)
}
}
}
}
var requests: [Signal<Api.messages.StickerSet?, NoError>] = []
for reference in stickerPacks {
if case let .id(id, accessHash) = reference {
requests.append(account.network.request(Api.functions.messages.getStickerSet(stickerset: .inputStickerSetID(id: id, accessHash: accessHash)))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.StickerSet?, NoError> in
return .single(nil)
})
}
}
if requests.isEmpty {
return .complete()
}
return combineLatest(requests)
|> mapToSignal { results -> Signal<Void, NoError> in
return account.postbox.transaction { transaction -> Void in
for result in results {
switch result {
case let .stickerSet(_, _, documents):
for document in documents {
if let file = telegramMediaFileFromApiDocument(document) {
if transaction.getMedia(file.fileId) != nil {
let _ = transaction.updateMedia(file.fileId, update: file)
}
}
}
default:
break
}
}
}
}
}
|> afterDisposed { [weak self] in
self?.queue.async {
self?.updatedUnsupportedMediaDisposables.set(nil, forKey: disposableId)
}
}
self.updatedUnsupportedMediaDisposables.set(signal.start(), forKey: disposableId)
}
}
}
}
}
public func updateMarkAllMentionsSeen(peerId: PeerId) {
self.queue.async {
guard let account = self.account else {

View File

@ -152,7 +152,7 @@ func mergeGroupOrChannel(lhs: Peer?, rhs: Api.Chat) -> Peer? {
return TelegramChannel(id: lhs.id, accessHash: lhs.accessHash, title: title, username: username, photo: imageRepresentationsForApiChatPhoto(photo), creationDate: lhs.creationDate, version: lhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: lhs.restrictionInfo, adminRights: lhs.adminRights, bannedRights: lhs.bannedRights, defaultBannedRights: defaultBannedRights.flatMap(TelegramChatBannedRights.init))
} else {
return nil
return parseTelegramGroupOrChannel(chat: rhs)
}
}
}
@ -188,6 +188,6 @@ func mergeChannel(lhs: TelegramChannel?, rhs: TelegramChannel) -> TelegramChanne
accessHash = rhs.accessHash ?? lhs.accessHash
}
return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: rhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights)
return TelegramChannel(id: lhs.id, accessHash: accessHash, title: rhs.title, username: rhs.username, photo: rhs.photo, creationDate: rhs.creationDate, version: rhs.version, participationStatus: lhs.participationStatus, info: info, flags: channelFlags, restrictionInfo: rhs.restrictionInfo, adminRights: rhs.adminRights, bannedRights: rhs.bannedRights, defaultBannedRights: rhs.defaultBannedRights)
}

View File

@ -223,6 +223,10 @@ public func channelAdminLogEvents(postbox: Postbox, network: Network, peerId: Pe
return postbox.transaction { transaction -> AdminLogEventsResult in
updatePeers(transaction: transaction, peers: peers.map { $0.1 }, update: { return $1 })
var peers = peers
if peers[peerId] == nil, let peer = transaction.getPeer(peerId) {
peers[peer.id] = peer
}
return AdminLogEventsResult(peerId: peerId, peers: peers, events: events)
} |> castError(MTRpcError.self)
}

View File

@ -18,6 +18,7 @@ public enum ChannelMembersCategory {
case bots(ChannelMembersCategoryFilter)
case restricted(ChannelMembersCategoryFilter)
case banned(ChannelMembersCategoryFilter)
case mentions(threadId: MessageId?, filter: ChannelMembersCategoryFilter)
}
public func channelMembers(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, category: ChannelMembersCategory = .recent(.all), offset: Int32 = 0, limit: Int32 = 64, hash: Int32 = 0) -> Signal<[RenderedChannelParticipant]?, NoError> {
@ -32,6 +33,24 @@ public func channelMembers(postbox: Postbox, network: Network, accountPeerId: Pe
case let .search(query):
apiFilter = .channelParticipantsSearch(q: query)
}
case let .mentions(threadId, filter):
switch filter {
case .all:
var flags: Int32 = 0
if threadId != nil {
flags |= 1 << 1
}
apiFilter = .channelParticipantsMentions(flags: flags, q: nil, topMsgId: threadId?.id)
case let .search(query):
var flags: Int32 = 0
if threadId != nil {
flags |= 1 << 1
}
if !query.isEmpty {
flags |= 1 << 0
}
apiFilter = .channelParticipantsMentions(flags: flags, q: query.isEmpty ? nil : query, topMsgId: threadId?.id)
}
case .admins:
apiFilter = .channelParticipantsAdmins
case let .contacts(filter):

View File

@ -130,6 +130,21 @@ private func requestActivity(postbox: Postbox, network: Network, accountPeerId:
if let channel = peer as? TelegramChannel, case .broadcast = channel.info {
return .complete()
}
if let _ = peer as? TelegramUser {
if let presence = transaction.getPeerPresence(peerId: peerId) as? TelegramUserPresence {
switch presence.status {
case .none, .recently, .lastWeek, .lastMonth:
return .complete()
case let .present(statusTimestamp):
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if statusTimestamp < timestamp {
return .complete()
}
}
} else {
return .complete()
}
}
if let inputPeer = apiInputPeer(peer) {
var flags: Int32 = 0

View File

@ -9,11 +9,13 @@ public struct MessageStats: Equatable {
public let views: Int
public let forwards: Int
public let interactionsGraph: StatsGraph
public let detailedInteractionsGraph: StatsGraph?
init(views: Int, forwards: Int, interactionsGraph: StatsGraph) {
init(views: Int, forwards: Int, interactionsGraph: StatsGraph, detailedInteractionsGraph: StatsGraph?) {
self.views = views
self.forwards = forwards
self.interactionsGraph = interactionsGraph
self.detailedInteractionsGraph = detailedInteractionsGraph
}
public static func == (lhs: MessageStats, rhs: MessageStats) -> Bool {
@ -26,11 +28,14 @@ public struct MessageStats: Equatable {
if lhs.interactionsGraph != rhs.interactionsGraph {
return false
}
if lhs.detailedInteractionsGraph != rhs.detailedInteractionsGraph {
return false
}
return true
}
public func withUpdatedInteractionsGraph(_ interactionsGraph: StatsGraph) -> MessageStats {
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: self.interactionsGraph)
return MessageStats(views: self.views, forwards: self.forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: self.detailedInteractionsGraph)
}
}
@ -39,8 +44,7 @@ public struct MessageStatsContextState: Equatable {
}
private func requestMessageStats(postbox: Postbox, network: Network, datacenterId: Int32, messageId: MessageId, dark: Bool = false) -> Signal<MessageStats?, NoError> {
return .single(nil)
/*return postbox.transaction { transaction -> (Peer, Message)? in
return postbox.transaction { transaction -> (Peer, Message)? in
if let peer = transaction.getPeer(messageId.peerId), let message = transaction.getMessage(messageId) {
return (peer, message)
} else {
@ -79,15 +83,25 @@ private func requestMessageStats(postbox: Postbox, network: Network, datacenterI
}
return signal
|> map { result -> MessageStats? in
|> mapToSignal { result -> Signal<MessageStats?, MTRpcError> in
if case let .messageStats(apiViewsGraph) = result {
return MessageStats(views: views, forwards: forwards, interactionsGraph: StatsGraph(apiStatsGraph: apiViewsGraph))
let interactionsGraph = StatsGraph(apiStatsGraph: apiViewsGraph)
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
if case let .Loaded(tokenValue, _) = interactionsGraph, let token = tokenValue, Int64(message.timestamp + 60 * 60 * 24 * 2) > Int64(timestamp) {
return requestGraph(network: network, datacenterId: datacenterId, token: token, x: 1601596800000)
|> castError(MTRpcError.self)
|> map { detailedGraph -> MessageStats? in
return MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: detailedGraph)
}
} else {
return .single(MessageStats(views: views, forwards: forwards, interactionsGraph: interactionsGraph, detailedInteractionsGraph: nil))
}
} else {
return nil
return .single(nil)
}
}
|> retryRequest
}*/
}
}
private final class MessageStatsContextImpl {

View File

@ -6,6 +6,7 @@ import TelegramApi
private struct DiscussionMessage {
public var messageId: MessageId
public var channelMessageId: MessageId?
public var isChannelPost: Bool
public var maxMessage: MessageId?
public var maxReadIncomingMessageId: MessageId?
@ -143,6 +144,16 @@ private class ReplyThreadHistoryContextImpl {
return .fail(.generic)
}
var channelMessageId: MessageId?
var replyThreadAttribute: ReplyThreadMessageAttribute?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
} else if let attribute = attribute as? ReplyThreadMessageAttribute {
replyThreadAttribute = attribute
}
}
var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:]
@ -186,13 +197,41 @@ private class ReplyThreadHistoryContextImpl {
}
}
let maxReadIncomingMessageId = readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
}
if let channelMessageId = channelMessageId, let replyThreadAttribute = replyThreadAttribute {
account.viewTracker.updateReplyInfoForMessageId(channelMessageId, info: AccountViewTracker.UpdatedMessageReplyInfo(
timestamp: Int32(CFAbsoluteTimeGetCurrent()),
commentsPeerId: parsedIndex.id.peerId,
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxMessageId: resolvedMaxMessage
))
transaction.updateMessage(channelMessageId, update: { currentMessage in
var attributes = currentMessage.attributes
loop: for j in 0 ..< attributes.count {
if let attribute = attributes[j] as? ReplyThreadMessageAttribute {
attributes[j] = ReplyThreadMessageAttribute(
count: replyThreadAttribute.count,
latestUsers: attribute.latestUsers,
commentsPeerId: attribute.commentsPeerId,
maxMessageId: replyThreadAttribute.maxMessageId,
maxReadMessageId: replyThreadAttribute.maxReadMessageId
)
}
}
return .update(StoreMessage(id: currentMessage.id, globallyUniqueId: currentMessage.globallyUniqueId, groupingKey: currentMessage.groupingKey, threadId: currentMessage.threadId, timestamp: currentMessage.timestamp, flags: StoreMessageFlags(currentMessage.flags), tags: currentMessage.tags, globalTags: currentMessage.globalTags, localTags: currentMessage.localTags, forwardInfo: currentMessage.forwardInfo.flatMap(StoreMessageForwardInfo.init), authorId: currentMessage.author?.id, text: currentMessage.text, attributes: attributes, media: currentMessage.media))
})
}
return .single(DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: readInboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
},
maxReadIncomingMessageId: maxReadIncomingMessageId,
maxReadOutgoingMessageId: readOutboxMaxId.flatMap { readMaxId in
MessageId(peerId: parsedIndex.id.peerId, namespace: Namespaces.Message.Cloud, id: readMaxId)
}
@ -388,6 +427,7 @@ public struct ChatReplyThreadMessage: Equatable {
}
public var messageId: MessageId
public var channelMessageId: MessageId?
public var isChannelPost: Bool
public var maxMessage: MessageId?
public var maxReadIncomingMessageId: MessageId?
@ -396,8 +436,9 @@ public struct ChatReplyThreadMessage: Equatable {
public var initialAnchor: Anchor
public var isNotAvailable: Bool
fileprivate init(messageId: MessageId, isChannelPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) {
fileprivate init(messageId: MessageId, channelMessageId: MessageId?, isChannelPost: Bool, maxMessage: MessageId?, maxReadIncomingMessageId: MessageId?, maxReadOutgoingMessageId: MessageId?, initialFilledHoles: IndexSet, initialAnchor: Anchor, isNotAvailable: Bool) {
self.messageId = messageId
self.channelMessageId = channelMessageId
self.isChannelPost = isChannelPost
self.maxMessage = maxMessage
self.maxReadIncomingMessageId = maxReadIncomingMessageId
@ -445,6 +486,14 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
return nil
}
var channelMessageId: MessageId?
for attribute in topMessage.attributes {
if let attribute = attribute as? SourceReferenceMessageAttribute {
channelMessageId = attribute.messageId
break
}
}
var peers: [Peer] = []
var peerPresences: [PeerId: PeerPresence] = [:]
@ -490,6 +539,7 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
return DiscussionMessage(
messageId: parsedIndex.id,
channelMessageId: channelMessageId,
isChannelPost: isChannelPost,
maxMessage: resolvedMaxMessage,
maxReadIncomingMessageId: readInboxMaxId.flatMap { readMaxId in
@ -530,6 +580,7 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
return DiscussionMessage(
messageId: discussionMessageId,
channelMessageId: messageId,
isChannelPost: true,
maxMessage: replyInfo.maxMessageId,
maxReadIncomingMessageId: replyInfo.maxReadIncomingMessageId,
@ -730,6 +781,7 @@ public func fetchChannelReplyThreadMessage(account: Account, messageId: MessageI
return .single(ChatReplyThreadMessage(
messageId: discussionMessage.messageId,
channelMessageId: discussionMessage.channelMessageId,
isChannelPost: discussionMessage.isChannelPost,
maxMessage: discussionMessage.maxMessage,
maxReadIncomingMessageId: discussionMessage.maxReadIncomingMessageId,

View File

@ -221,11 +221,11 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
guard let inputPeer = apiInputPeer(peer) else {
return .single((nil, nil))
}
var fromInputUser: Api.InputUser? = nil
var fromInputPeer: Api.InputPeer? = nil
var flags: Int32 = 0
if let from = values.from {
fromInputUser = apiInputUser(from)
if let _ = fromInputUser {
fromInputPeer = apiInputPeer(from)
if let _ = fromInputPeer {
flags |= (1 << 0)
}
}
@ -241,7 +241,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
if peer.id.namespace == Namespaces.Peer.CloudChannel && query.isEmpty && fromId == nil && tags == nil && minDate == nil && maxDate == nil {
signal = account.network.request(Api.functions.messages.getHistory(peer: inputPeer, offsetId: lowerBound?.id.id ?? 0, offsetDate: 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
} else {
signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputUser, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
signal = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
}
peerMessages = signal
|> map(Optional.init)
@ -257,7 +257,7 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
additionalPeerMessages = .single(nil)
} else if mainCompleted || !hasAdditional {
let lowerBound = state?.additional?.messages.last.flatMap({ $0.index })
additionalPeerMessages = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputUser, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
additionalPeerMessages = account.network.request(Api.functions.messages.search(flags: flags, peer: inputPeer, q: query, fromId: fromInputPeer, topMsgId: topMsgId?.id, filter: filter, minDate: minDate ?? 0, maxDate: maxDate ?? (Int32.max - 1), offsetId: lowerBound?.id.id ?? 0, addOffset: 0, limit: limit, maxId: Int32.max - 1, minId: 0, hash: 0))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.messages.Messages?, NoError> in
return .single(nil)
@ -345,15 +345,32 @@ public func searchMessages(account: Account, location: SearchMessagesLocation, q
|> mapToSignal { result, additionalResult -> Signal<(SearchMessagesResult, SearchMessagesState), NoError> in
return account.postbox.transaction { transaction -> (SearchMessagesResult, SearchMessagesState) in
var additional: SearchMessagesPeerState? = mergedState(transaction: transaction, state: state?.additional, result: additionalResult)
if state?.additional == nil, case let .general(tags, _, _) = location {
let secretMessages = transaction.searchMessages(peerId: nil, query: query, tags: tags)
var readStates: [PeerId: CombinedPeerReadState] = [:]
for message in secretMessages {
if let readState = transaction.getCombinedPeerReadState(message.id.peerId) {
readStates[message.id.peerId] = readState
}
if state?.additional == nil {
switch location {
case let .general(tags, minDate, maxDate), let .group(_, tags, minDate, maxDate):
let secretMessages = transaction.searchMessages(peerId: nil, query: query, tags: tags)
var filteredMessages: [Message] = []
var readStates: [PeerId: CombinedPeerReadState] = [:]
for message in secretMessages {
var match = true
if let minDate = minDate, message.timestamp < minDate {
match = false
}
if let maxDate = maxDate, message.timestamp > maxDate {
match = false
}
if match {
filteredMessages.append(message)
if let readState = transaction.getCombinedPeerReadState(message.id.peerId) {
readStates[message.id.peerId] = readState
}
}
}
additional = SearchMessagesPeerState(messages: filteredMessages, readStates: readStates, totalCount: Int32(filteredMessages.count), completed: true, nextRate: nil)
default:
break
}
additional = SearchMessagesPeerState(messages: secretMessages, readStates: readStates, totalCount: Int32(secretMessages.count), completed: true, nextRate: nil)
}
let updatedState = SearchMessagesState(main: mergedState(transaction: transaction, state: state?.main, result: result) ?? SearchMessagesPeerState(messages: [], readStates: [:], totalCount: 0, completed: true, nextRate: nil), additional: additional)

View File

@ -210,7 +210,7 @@ public class BoxedMessage: NSObject {
public class Serialization: NSObject, MTSerialization {
public func currentLayer() -> UInt {
return 119
return 120
}
public func parseMessage(_ data: Data!) -> Any! {

View File

@ -38,7 +38,7 @@
function getCurrentTime() {
downloadProgress = player.getVideoLoadedFraction();
position = player.getCurrentTime();
storyboardSpec = player.getStoryboardFormat();
//storyboardSpec = player.getStoryboardFormat();
updateState();
invoke("tick");
@ -67,6 +67,7 @@
function onReady(event) {
window.location.href = "embed://onReady?data=" + event.data;
iframe = document.getElementById("player");
iframe.referrerPolicy = "origin";
duration = player.getDuration();
invoke("tick");
}

View File

@ -16,56 +16,56 @@ function initialize() {
function tick() {
var watermark = document.getElementsByClassName("ytp-watermark")[0];
if (watermark != null) {
watermark.style.display = "none";
// watermark.style.display = "none";
}
var button = document.getElementsByClassName("ytp-large-play-button")[0];
if (button != null) {
button.style.display = "none";
button.style.opacity = "0";
// button.style.display = "none";
// button.style.opacity = "0";
}
var progress = document.getElementsByClassName("ytp-spinner-container")[0];
if (progress != null) {
progress.style.display = "none";
progress.style.opacity = "0";
// progress.style.display = "none";
// progress.style.opacity = "0";
}
var pause = document.getElementsByClassName("ytp-pause-overlay")[0];
if (pause != null) {
pause.style.display = "none";
pause.style.opacity = "0";
// pause.style.display = "none";
// pause.style.opacity = "0";
}
var chrome = document.getElementsByClassName("ytp-chrome-top")[0];
if (chrome != null) {
chrome.style.display = "none";
chrome.style.opacity = "0";
// chrome.style.display = "none";
// chrome.style.opacity = "0";
}
var paid = document.getElementsByClassName("ytp-paid-content-overlay")[0];
if (paid != null) {
paid.style.display = "none";
paid.style.opacity = "0";
// paid.style.display = "none";
// paid.style.opacity = "0";
}
var gradient = document.getElementsByClassName("ytp-gradient-top")[0];
if (gradient != null) {
gradient.style.display = "none";
gradient.style.opacity = "0";
// gradient.style.display = "none";
// gradient.style.opacity = "0";
}
var end = document.getElementsByClassName("html5-endscreen")[0];
if (end != null) {
end.style.display = "none";
end.style.opacity = "0";
// end.style.display = "none";
// end.style.opacity = "0";
}
var elements = document.getElementsByClassName("ytp-ce-element");
for (var i = 0; i < elements.length; i++) {
var element = elements[i]
element.style.display = "none";
element.style.opacity = "0";
// element.style.display = "none";
// element.style.opacity = "0";
}
var video = document.getElementsByTagName("video")[0];

View File

@ -289,6 +289,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
private var stickerSettingsDisposable: Disposable?
private var applicationInForegroundDisposable: Disposable?
private var applicationInFocusDisposable: Disposable?
private var checkedPeerChatServiceActions = false
@ -2219,6 +2220,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
break
}
}
}, openMessageStats: { [weak self] id in
let _ = (context.account.postbox.transaction { transaction -> CachedPeerData? in
return transaction.getPeerCachedData(peerId: id.peerId)
} |> deliverOnMainQueue).start(next: { [weak self] cachedPeerData in
guard let strongSelf = self, let cachedPeerData = cachedPeerData else {
return
}
strongSelf.push(messageStatsController(context: context, messageId: id, cachedPeerData: cachedPeerData))
})
}, requestMessageUpdate: { [weak self] id in
if let strongSelf = self {
strongSelf.chatDisplayNode.historyNode.requestMessageUpdate(id)
@ -3058,6 +3068,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
})
var wasInForeground = true
self.applicationInForegroundDisposable = (context.sharedContext.applicationBindings.applicationInForeground
|> distinctUntilChanged
|> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in
@ -3067,10 +3078,26 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.raiseToListen?.applicationResignedActive()
strongSelf.stopMediaRecorder()
} else {
if !wasInForeground {
strongSelf.chatDisplayNode.recursivelyEnsureDisplaySynchronously(true)
}
}
wasInForeground = value
}
})
if case let .peer(peerId) = chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
self.applicationInFocusDisposable = (context.sharedContext.applicationBindings.applicationIsActive
|> distinctUntilChanged
|> deliverOn(Queue.mainQueue())).start(next: { [weak self] value in
guard let strongSelf = self, strongSelf.isNodeLoaded else {
return
}
strongSelf.chatDisplayNode.updateIsBlurred(!value)
})
}
self.canReadHistoryDisposable = (combineLatest(context.sharedContext.applicationBindings.applicationInForeground, self.canReadHistory.get()) |> map { a, b in
return a && b
} |> deliverOnMainQueue).start(next: { [weak self] value in
@ -3139,6 +3166,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.presentationDataDisposable?.dispose()
self.searchDisposable?.dispose()
self.applicationInForegroundDisposable?.dispose()
self.applicationInFocusDisposable?.dispose()
self.canReadHistoryDisposable?.dispose()
self.networkStateDisposable?.dispose()
self.chatAdditionalDataDisposable.dispose()
@ -3255,13 +3283,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
if case let .replyThread(replyThreadMessageId) = strongSelf.chatLocation {
pinnedMessageId = replyThreadMessageId.messageId
pinnedMessageId = replyThreadMessageId.effectiveTopId
}
var pinnedMessage: Message?
var pinnedMessage: ChatPinnedMessage?
if let pinnedMessageId = pinnedMessageId {
if let cachedDataMessages = combinedInitialData.cachedDataMessages {
pinnedMessage = cachedDataMessages[pinnedMessageId]
if let message = cachedDataMessages[pinnedMessageId] {
pinnedMessage = ChatPinnedMessage(message: message, isLatest: true)
}
}
}
@ -3372,7 +3402,62 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let isTopReplyThreadMessageShown: Signal<Bool, NoError> = self.chatDisplayNode.historyNode.isTopReplyThreadMessageShown.get()
|> distinctUntilChanged
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown).start(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown in
let topPinnedMessage: Signal<ChatPinnedMessage?, NoError>
switch self.chatLocation {
case let .peer(peerId):
let replyHistory: Signal<ChatHistoryViewUpdate, NoError> = (chatHistoryViewForLocation(ChatHistoryLocationInput(content: .Initial(count: 100), id: 0), context: self.context, chatLocation: .peer(peerId), chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), scheduled: false, fixedCombinedReadStates: nil, tagMask: MessageTags.photoOrVideo, additionalData: [])
|> castError(Bool.self)
|> mapToSignal { update -> Signal<ChatHistoryViewUpdate, Bool> in
switch update {
case let .Loading(_, type):
if case .Generic(.FillHole) = type {
return .fail(true)
}
case let .HistoryView(_, type, _, _, _, _, _):
if case .Generic(.FillHole) = type {
return .fail(true)
}
}
return .single(update)
})
|> restartIfError
topPinnedMessage = combineLatest(
replyHistory,
self.chatDisplayNode.historyNode.topVisibleMessage.get()
)
|> map { update, topVisibleMessage -> ChatPinnedMessage? in
var message: ChatPinnedMessage?
switch update {
case .Loading:
break
case let .HistoryView(view, _, _, _, _, _, _):
for i in 0 ..< view.entries.count {
let entry = view.entries[i]
var matches = false
if message == nil {
matches = true
} else if let topVisibleMessage = topVisibleMessage {
if entry.message.id < topVisibleMessage.id {
matches = true
}
} else {
matches = true
}
if matches {
message = ChatPinnedMessage(message: entry.message, isLatest: i == view.entries.count - 1)
}
}
break
}
return message
}
|> distinctUntilChanged
case .replyThread:
topPinnedMessage = .single(nil)
}
self.cachedDataDisposable = combineLatest(queue: .mainQueue(), self.chatDisplayNode.historyNode.cachedPeerDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage).start(next: { [weak self] cachedDataAndMessages, hasPendingMessages, isTopReplyThreadMessageShown, topPinnedMessage in
if let strongSelf = self {
let (cachedData, messages) = cachedDataAndMessages
@ -3400,22 +3485,31 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} else if let _ = cachedData as? CachedSecretChatData {
}
var pinnedMessage: ChatPinnedMessage?
if case let .replyThread(replyThreadMessage) = strongSelf.chatLocation {
if isTopReplyThreadMessageShown {
pinnedMessageId = nil
} else {
pinnedMessageId = replyThreadMessage.messageId
pinnedMessageId = replyThreadMessage.effectiveTopId
}
}
var pinnedMessage: Message?
if let pinnedMessageId = pinnedMessageId {
pinnedMessage = messages?[pinnedMessageId]
if let pinnedMessageId = pinnedMessageId {
if let message = messages?[pinnedMessageId] {
pinnedMessage = ChatPinnedMessage(message: message, isLatest: true)
}
}
} else {
if let pinnedMessageId = pinnedMessageId {
if let message = messages?[pinnedMessageId] {
pinnedMessage = ChatPinnedMessage(message: message, isLatest: true)
}
}
//pinnedMessageId = topPinnedMessage?.message.id
//pinnedMessage = topPinnedMessage
}
var pinnedMessageUpdated = false
if let current = strongSelf.presentationInterfaceState.pinnedMessage, let updated = pinnedMessage {
if current.id != updated.id || current.stableVersion != updated.stableVersion {
if current != updated {
pinnedMessageUpdated = true
}
} else if (strongSelf.presentationInterfaceState.pinnedMessage != nil) != (pinnedMessage != nil) {
@ -3424,7 +3518,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let callsDataUpdated = strongSelf.presentationInterfaceState.callsAvailable != callsAvailable || strongSelf.presentationInterfaceState.callsPrivate != callsPrivate
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage?.stableVersion != pinnedMessage?.stableVersion || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState {
if strongSelf.presentationInterfaceState.pinnedMessageId != pinnedMessageId || strongSelf.presentationInterfaceState.pinnedMessage != pinnedMessage || strongSelf.presentationInterfaceState.peerIsBlocked != peerIsBlocked || pinnedMessageUpdated || callsDataUpdated || strongSelf.presentationInterfaceState.slowmodeState != slowmodeState {
strongSelf.updateChatPresentationInterfaceState(animated: strongSelf.willAppear, interactive: strongSelf.willAppear, { state in
return state
.updatedPinnedMessageId(pinnedMessageId)
@ -4721,7 +4815,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})]), in: .window(.root))
}
} else {
if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.id {
if let pinnedMessageId = strongSelf.presentationInterfaceState.pinnedMessage?.message.id {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
var value = value
@ -4773,7 +4867,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, {
return $0.updatedInterfaceState({ $0.withUpdatedMessageActionsState({ value in
var value = value
value.closedPinnedMessageId = pinnedMessage.id
value.closedPinnedMessageId = pinnedMessage.message.id
return value
}) })
})
@ -6945,6 +7039,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if let strongSelf = self {
let replyMessageId = strongSelf.presentationInterfaceState.interfaceState.replyMessageId
var groupingKey: Int64?
var allItemsAreAudio = true
for item in results {
if let item = item {
let pathExtension = (item.fileName as NSString).pathExtension.lowercased()
if !["mp3", "m4a"].contains(pathExtension) {
allItemsAreAudio = false
}
}
}
if allItemsAreAudio {
groupingKey = arc4random64()
}
var messages: [EnqueueMessage] = []
for item in results {
if let item = item {
@ -6954,8 +7063,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if mimeType == "application/pdf" {
previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 320, height: 320), resource: ICloudFileResource(urlData: item.urlData, thumbnail: true), progressiveSizes: []))
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: [.FileName(fileName: item.fileName)])
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: nil)
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.FileName(fileName: item.fileName))
if let audioMetadata = item.audioMetadata {
attributes.append(.Audio(isVoice: false, duration: audioMetadata.duration, title: audioMetadata.title, performer: audioMetadata.performer, waveform: nil))
}
let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: fileId), partialReference: nil, resource: ICloudFileResource(urlData: item.urlData, thumbnail: false), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: item.fileSize, attributes: attributes)
let message: EnqueueMessage = .message(text: "", attributes: [], mediaReference: .standalone(media: file), replyToMessageId: replyMessageId, localGroupingKey: groupingKey)
messages.append(message)
}
}
@ -6963,9 +7078,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
if !messages.isEmpty {
if editingMessage {
strongSelf.editMessageMediaWithMessages(messages)
} else {
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
strongSelf.chatDisplayNode.setupSendActionOnViewUpdate({
if let strongSelf = self {
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: false, {
$0.updatedInterfaceState { $0.withUpdatedReplyMessageId(nil) }
@ -8461,7 +8575,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}, error: { _ in
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
present(textAlertController(context: context, title: nil, text: presentationData.strings.Login_UnknownError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
present(textAlertController(context: context, title: nil, text: presentationData.strings.Channel_DiscussionMessageUnavailable, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), nil)
})
cancelImpl = { [weak statusController] in

View File

@ -114,6 +114,7 @@ public final class ChatControllerInteraction {
let openPeerContextMenu: (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void
let openMessageReplies: (MessageId, Bool, Bool) -> Void
let openReplyThreadOriginalMessage: (Message) -> Void
let openMessageStats: (MessageId) -> Void
let requestMessageUpdate: (MessageId) -> Void
let cancelInteractiveKeyboardGestures: () -> Void
@ -199,6 +200,7 @@ public final class ChatControllerInteraction {
openPeerContextMenu: @escaping (Peer, ASDisplayNode, CGRect, ContextGesture?) -> Void,
openMessageReplies: @escaping (MessageId, Bool, Bool) -> Void,
openReplyThreadOriginalMessage: @escaping (Message) -> Void,
openMessageStats: @escaping (MessageId) -> Void,
requestMessageUpdate: @escaping (MessageId) -> Void,
cancelInteractiveKeyboardGestures: @escaping () -> Void,
automaticMediaDownloadSettings: MediaAutoDownloadSettings,
@ -271,6 +273,7 @@ public final class ChatControllerInteraction {
self.openPeerContextMenu = openPeerContextMenu
self.openMessageReplies = openMessageReplies
self.openReplyThreadOriginalMessage = openReplyThreadOriginalMessage
self.openMessageStats = openMessageStats
self.requestMessageUpdate = requestMessageUpdate
self.cancelInteractiveKeyboardGestures = cancelInteractiveKeyboardGestures
@ -321,6 +324,7 @@ public final class ChatControllerInteraction {
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -14,6 +14,7 @@ import TelegramNotices
import ReactionSelectionNode
import TelegramUniversalVideoContent
import ChatInterfaceState
import FastBlur
final class VideoNavigationControllerDropContentItem: NavigationControllerDropContentItem {
let itemNode: OverlayMediaItemNode
@ -309,6 +310,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
let backgroundNode: WallpaperBackgroundNode
let backgroundImageDisposable = MetaDisposable()
let historyNode: ChatHistoryListNode
var blurredHistoryNode: ASImageNode?
let reactionContainerNode: ReactionSelectionParentNode
let historyNodeContainer: ASDisplayNode
let loadingNode: ChatLoadingNode
@ -995,7 +997,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
}
}
if let pinnedMessage = self.chatPresentationInterfaceState.pinnedMessage, self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding, self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback, self.embeddedTitleContentNode == nil, let url = extractExperimentalPlaylistUrl(pinnedMessage.text), self.didProcessExperimentalEmbedUrl != url {
if let pinnedMessage = self.chatPresentationInterfaceState.pinnedMessage, self.context.sharedContext.immediateExperimentalUISettings.playerEmbedding, self.context.sharedContext.immediateExperimentalUISettings.playlistPlayback, self.embeddedTitleContentNode == nil, let url = extractExperimentalPlaylistUrl(pinnedMessage.message.text), self.didProcessExperimentalEmbedUrl != url {
self.didProcessExperimentalEmbedUrl = url
let context = self.context
let baseNavigationController = self.controller?.navigationController as? NavigationController
@ -1220,6 +1222,9 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds)
transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size))
transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0))
if let blurredHistoryNode = self.blurredHistoryNode {
transition.updateFrame(node: blurredHistoryNode, frame: contentBounds)
}
transition.updateFrame(node: self.loadingNode, frame: contentBounds)
@ -2933,4 +2938,39 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
self.updateHasEmbeddedTitleContent?()
}
}
func updateIsBlurred(_ isBlurred: Bool) {
if isBlurred {
if self.blurredHistoryNode == nil {
let unscaledSize = self.historyNode.frame.size
let image = generateImage(CGSize(width: floor(unscaledSize.width), height: floor(unscaledSize.height)), opaque: true, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
UIGraphicsPushContext(context)
let backgroundFrame = self.backgroundNode.view.convert(self.backgroundNode.bounds, to: self.historyNode.supernode?.view)
self.backgroundNode.view.drawHierarchy(in: backgroundFrame, afterScreenUpdates: false)
context.translateBy(x: size.width / 2.0, y: size.height / 2.0)
context.scaleBy(x: -1.0, y: -1.0)
context.translateBy(x: -size.width / 2.0, y: -size.height / 2.0)
self.historyNode.view.drawHierarchy(in: CGRect(origin: CGPoint(), size: unscaledSize), afterScreenUpdates: false)
UIGraphicsPopContext()
}).flatMap(applyScreenshotEffectToImage)
let blurredHistoryNode = ASImageNode()
blurredHistoryNode.image = image
blurredHistoryNode.frame = self.historyNode.frame
self.blurredHistoryNode = blurredHistoryNode
self.historyNode.supernode?.insertSubnode(blurredHistoryNode, aboveSubnode: self.historyNode)
}
} else {
if let blurredHistoryNode = self.blurredHistoryNode {
self.blurredHistoryNode = nil
blurredHistoryNode.removeFromSupernode()
}
}
self.historyNode.isHidden = isBlurred
}
}

View File

@ -139,7 +139,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
if case let .replyThread(replyThreadMessage) = location, view.earlierId == nil, !view.holeEarlier, !view.isLoading {
loop: for entry in view.additionalData {
switch entry {
case let .message(id, messages) where id == replyThreadMessage.messageId:
case let .message(id, messages) where id == replyThreadMessage.effectiveTopId:
if !messages.isEmpty {
let selection: ChatHistoryMessageSelection = .none

View File

@ -18,6 +18,17 @@ import ListMessageItem
import AccountContext
import ChatInterfaceState
extension ChatReplyThreadMessage {
var effectiveTopId: MessageId {
return self.channelMessageId ?? self.messageId
}
}
struct ChatTopVisibleMessage: Equatable {
var id: MessageId
var isLast: Bool
}
private class ChatHistoryListSelectionRecognizer: UIPanGestureRecognizer {
private let selectionGestureActivationThreshold: CGFloat = 5.0
@ -503,6 +514,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private let messageReactionsProcessingManager = ChatMessageThrottledProcessingManager()
private let seenLiveLocationProcessingManager = ChatMessageThrottledProcessingManager()
private let unsupportedMessageProcessingManager = ChatMessageThrottledProcessingManager()
private let refreshMediaProcessingManager = ChatMessageThrottledProcessingManager()
private let messageMentionProcessingManager = ChatMessageThrottledProcessingManager(delay: 0.2)
let prefetchManager: InChatPrefetchManager
private var currentEarlierPrefetchMessages: [(Message, Media)] = []
@ -551,6 +563,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private var loadedMessagesFromCachedDataDisposable: Disposable?
let isTopReplyThreadMessageShown = ValuePromise<Bool>(false, ignoreRepeated: true)
let topVisibleMessage = ValuePromise<ChatTopVisibleMessage?>(nil, ignoreRepeated: true)
private let clientId: Atomic<Int32>
@ -593,6 +606,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.unsupportedMessageProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateUnsupportedMediaForMessageIds(messageIds: messageIds)
}
self.refreshMediaProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.refreshSecretMediaMediaForMessageIds(messageIds: messageIds)
}
self.messageMentionProcessingManager.process = { [weak context] messageIds in
context?.account.viewTracker.updateMarkMentionsSeenForMessageIds(messageIds: messageIds)
}
@ -643,7 +659,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
additionalData.append(.peer(replyThreadMessage.messageId.peerId))
}
additionalData.append(.message(replyThreadMessage.messageId))
additionalData.append(.message(replyThreadMessage.effectiveTopId))
}
let currentViewVersion = Atomic<Int?>(value: nil)
@ -1145,6 +1161,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
private func processDisplayedItemRangeChanged(displayedRange: ListViewDisplayedItemRange, transactionState: ChatHistoryTransactionOpaqueState) {
let historyView = transactionState.historyView
var isTopReplyThreadMessageShownValue = false
var topVisibleMessage: ChatTopVisibleMessage?
if let visible = displayedRange.visibleRange {
let indexRange = (historyView.filteredEntries.count - 1 - visible.lastIndex, historyView.filteredEntries.count - 1 - visible.firstIndex)
if indexRange.0 > indexRange.1 {
@ -1161,6 +1178,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
var messageIdsWithUpdateableReactions: [MessageId] = []
var messageIdsWithLiveLocation: [MessageId] = []
var messageIdsWithUnsupportedMedia: [MessageId] = []
var messageIdsWithRefreshMedia: [MessageId] = []
var messageIdsWithUnseenPersonalMention: [MessageId] = []
var messagesWithPreloadableMediaToEarlier: [(Message, Media)] = []
var messagesWithPreloadableMediaToLater: [(Message, Media)] = []
@ -1179,6 +1197,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
var contentRequiredValidation = false
var mediaRequiredValidation = false
for attribute in message.attributes {
if attribute is ViewCountMessageAttribute {
if message.id.namespace == Namespaces.Message.Cloud {
@ -1205,6 +1224,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
} else if let _ = media as? TelegramMediaAction {
isAction = true
} else if let telegramFile = media as? TelegramMediaFile {
if telegramFile.isAnimatedSticker, (message.id.peerId.namespace == Namespaces.Peer.SecretChat || !telegramFile.previewRepresentations.isEmpty), let size = telegramFile.size, size > 0 && size <= 128 * 1024 {
if message.id.peerId.namespace == Namespaces.Peer.SecretChat {
if telegramFile.fileId.namespace == Namespaces.Media.CloudFile {
var isValidated = false
attributes: for attribute in telegramFile.attributes {
if case .hintIsValidated = attribute {
isValidated = true
break attributes
}
}
if !isValidated {
mediaRequiredValidation = true
}
}
}
}
}
}
if !isAction && message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
@ -1213,12 +1250,16 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if contentRequiredValidation {
messageIdsWithUnsupportedMedia.append(message.id)
}
if mediaRequiredValidation {
messageIdsWithRefreshMedia.append(message.id)
}
if hasUnconsumedMention && !hasUnconsumedContent {
messageIdsWithUnseenPersonalMention.append(message.id)
}
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId == message.id {
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
isTopReplyThreadMessageShownValue = true
}
topVisibleMessage = ChatTopVisibleMessage(id: message.id, isLast: i == historyView.filteredEntries.count - 1)
case let .MessageGroupEntry(_, messages, _):
for (message, _, _, _) in messages {
var hasUnconsumedMention = false
@ -1246,9 +1287,10 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if hasUnconsumedMention && !hasUnconsumedContent {
messageIdsWithUnseenPersonalMention.append(message.id)
}
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.messageId == message.id {
if case let .replyThread(replyThreadMessage) = self.chatLocation, replyThreadMessage.effectiveTopId == message.id {
isTopReplyThreadMessageShownValue = true
}
topVisibleMessage = ChatTopVisibleMessage(id: message.id, isLast: i == historyView.filteredEntries.count - 1)
}
default:
break
@ -1331,6 +1373,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
if !messageIdsWithUnsupportedMedia.isEmpty {
self.unsupportedMessageProcessingManager.add(messageIdsWithUnsupportedMedia)
}
if !messageIdsWithRefreshMedia.isEmpty {
self.refreshMediaProcessingManager.add(messageIdsWithRefreshMedia)
}
if !messageIdsWithUnseenPersonalMention.isEmpty {
self.messageMentionProcessingManager.add(messageIdsWithUnseenPersonalMention)
}
@ -1365,6 +1410,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
}
}
self.isTopReplyThreadMessageShown.set(isTopReplyThreadMessageShownValue)
self.topVisibleMessage.set(topVisibleMessage)
if let loaded = displayedRange.loadedRange, let firstEntry = historyView.filteredEntries.first, let lastEntry = historyView.filteredEntries.last {
if loaded.firstIndex < 5 && historyView.originalView.laterId != nil {

View File

@ -457,7 +457,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
var isReplyThreadHead = false
if case let .replyThread(replyThreadMessage) = chatPresentationInterfaceState.chatLocation {
isReplyThreadHead = messages[0].id == replyThreadMessage.messageId
isReplyThreadHead = messages[0].id == replyThreadMessage.effectiveTopId
}
if !isReplyThreadHead, data.canReply {
@ -605,7 +605,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
})
})))
}
if data.canEdit {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_MessageDialogEdit, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Edit"), color: theme.actionSheet.primaryTextColor)
@ -643,7 +643,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
if data.canPin, case .peer = chatPresentationInterfaceState.chatLocation {
if chatPresentationInterfaceState.pinnedMessage?.id != messages[0].id {
if chatPresentationInterfaceState.pinnedMessage?.message.id != messages[0].id {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_Pin, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Pin"), color: theme.actionSheet.primaryTextColor)
}, action: { _, f in
@ -808,9 +808,26 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState:
}
var clearCacheAsDelete = false
if let _ = message.peers[message.id.peerId] as? TelegramChannel {
if message.id.peerId.namespace == Namespaces.Peer.CloudChannel {
var views: Int = 0
for attribute in message.attributes {
if let attribute = attribute as? ViewCountMessageAttribute {
views = attribute.count
}
}
if views >= 100 {
actions.append(.action(ContextMenuActionItem(text: chatPresentationInterfaceState.strings.Conversation_ContextViewStats, icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Statistics"), color: theme.actionSheet.primaryTextColor)
}, action: { c, _ in
c.dismiss(completion: {
controllerInteraction.openMessageStats(messages[0].id)
})
})))
}
clearCacheAsDelete = true
}
if !isReplyThreadHead, (!data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty || clearCacheAsDelete) && !isAction {
let title: String
var isSending = false

View File

@ -145,7 +145,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, pee
}
let inlineBots: Signal<[(Peer, Double)], NoError> = types.contains(.contextBots) ? recentlyUsedInlineBots(postbox: context.account.postbox) : .single([])
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query))
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatLocation, query: query, scope: .mention))
|> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in
if rating < 0.14 {
@ -347,7 +347,7 @@ func searchQuerySuggestionResultStateForChatInterfacePresentationState(_ chatPre
}
}
let participants = searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatPresentationInterfaceState.chatLocation, query: query)
let participants = searchPeerMembers(context: context, peerId: peer.id, chatLocation: chatPresentationInterfaceState.chatLocation, query: query, scope: .memberSuggestion)
|> map { peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
let filteredPeers = peers
var sortedPeers: [Peer] = []

View File

@ -19,7 +19,7 @@ func titlePanelForChatPresentationInterfaceState(_ chatPresentationInterfaceStat
loop: for context in chatPresentationInterfaceState.titlePanelContexts.reversed() {
switch context {
case .pinnedMessage:
if let pinnedMessage = chatPresentationInterfaceState.pinnedMessage, pinnedMessage.id != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId {
if let pinnedMessage = chatPresentationInterfaceState.pinnedMessage, pinnedMessage.message.id != chatPresentationInterfaceState.interfaceState.messageActionsState.closedPinnedMessageId {
selectedContext = context
break loop
}

View File

@ -55,7 +55,7 @@ class ChatMessageShareButton: HighlightableButtonNode {
fatalError("init(coder:) has not been implemented")
}
func update(presentationData: ChatPresentationData, message: Message, account: Account) -> CGSize {
func update(presentationData: ChatPresentationData, chatLocation: ChatLocation, message: Message, account: Account) -> CGSize {
var isReplies = false
var replyCount = 0
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
@ -67,6 +67,10 @@ class ChatMessageShareButton: HighlightableButtonNode {
}
}
}
if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.effectiveTopId == message.id {
replyCount = 0
isReplies = false
}
if self.theme !== presentationData.theme.theme || self.isReplies != isReplies {
self.theme = presentationData.theme.theme
@ -569,7 +573,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if replyThreadMessage.isChannelPost, replyThreadMessage.messageId == item.message.id {
if replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id {
isBroadcastChannel = true
}
@ -925,7 +929,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
strongSelf.addSubnode(updatedShareButtonNode)
updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside)
}
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, message: item.message, account: item.context.account)
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, message: item.message, account: item.context.account)
updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: updatedImageFrame.maxX + 8.0, y: updatedImageFrame.maxY - buttonSize.height - 4.0), size: buttonSize)
} else if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.removeFromSupernode()
@ -1361,7 +1365,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
return
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
return
}

View File

@ -474,7 +474,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
}
}
let (_, refineLayout) = contentFileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
let (_, refineLayout) = contentFileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, associatedData.forcedResourceStatus, statusType, nil, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
refineContentFileLayout = refineLayout
}
} else if let image = media as? TelegramMediaImage {
@ -531,7 +531,7 @@ final class ChatMessageAttachedContentNode: ASDisplayNode {
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText)
statusInText = !imageMode

View File

@ -46,9 +46,14 @@ enum ChatMessageBubbleRelativePosition {
case freeform
}
enum NeighbourSpacing {
case `default`
case condensed
}
case None(ChatMessageBubbleMergeStatus)
case BubbleNeighbour
case Neighbour(Bool, NeighbourType)
case Neighbour(Bool, NeighbourType, NeighbourSpacing)
}
enum ChatMessageBubbleContentMosaicNeighbor {

View File

@ -32,6 +32,7 @@ enum InternalBubbleTapAction {
private struct BubbleItemAttributes {
var isAttachment: Bool
var neighborType: ChatMessageBubbleRelativePosition.NeighbourType
var neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing
}
private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(Message, AnyClass, ChatMessageEntryAttributes, BubbleItemAttributes)] {
@ -41,52 +42,61 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(
var isUnsupportedMedia = false
var isAction = false
var previousItemIsMusic = false
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)))
result.append((message, ChatMessageRestrictedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
break outer
}
}
inner: for media in message.media {
var isMusic = false
if let _ = media as? TelegramMediaImage {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media)))
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else if let file = media as? TelegramMediaFile {
let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil)
if isVideo {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media)))
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
var neighborSpacing: ChatMessageBubbleRelativePosition.NeighbourSpacing = .default
if previousItemIsMusic {
neighborSpacing = .condensed
}
isMusic = file.isMusic
result.append((message, ChatMessageFileBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: neighborSpacing)))
}
} 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)))
result.append((message, ChatMessageCallBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
} else if let _ = media as? TelegramMediaMap {
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageMapBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if let _ = media as? TelegramMediaGame {
skipText = true
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageGameBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
break inner
} else if let _ = media as? TelegramMediaInvoice {
skipText = true
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageInvoiceBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
break inner
} else if let _ = media as? TelegramMediaContact {
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageContactBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if let _ = media as? TelegramMediaExpiredContent {
result.removeAll()
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
return result
} else if let _ = media as? TelegramMediaPoll {
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessagePollBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else if let _ = media as? TelegramMediaUnsupported {
isUnsupportedMedia = true
}
previousItemIsMusic = isMusic
}
var messageText = message.text
@ -100,7 +110,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(
messageWithCaptionToAdd = (message, itemAttributes)
skipText = true
} else {
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
} else {
if case .group = item.content {
@ -112,29 +122,29 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(
inner: for media in message.media {
if let webpage = media as? TelegramMediaWebpage {
if case .Loaded = webpage.content {
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
break inner
}
}
if isUnsupportedMedia {
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((message, ChatMessageUnsupportedBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
}
if let (messageWithCaptionToAdd, itemAttributes) = messageWithCaptionToAdd {
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((messageWithCaptionToAdd, ChatMessageTextBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
if let additionalContent = item.additionalContent {
switch additionalContent {
case let .eventLogPreviousMessage(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((previousMessage, ChatMessageEventLogPreviousMessageContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
case let .eventLogPreviousDescription(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((previousMessage, ChatMessageEventLogPreviousDescriptionContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
case let .eventLogPreviousLink(previousMessage):
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform)))
result.append((previousMessage, ChatMessageEventLogPreviousLinkContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
}
@ -144,6 +154,9 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(
if let channel = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case let .broadcast(info) = channel.info, info.flags.contains(.hasDiscussionGroup) {
hasDiscussion = true
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == firstMessage.id {
hasDiscussion = false
}
if hasDiscussion {
var canComment = false
@ -164,10 +177,10 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> [(
}
if canComment {
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform)))
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
}
} else if firstMessage.id.peerId.isReplies {
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform)))
result.append((firstMessage, ChatMessageCommentFooterContentNode.self, ChatMessageEntryAttributes(), BubbleItemAttributes(isAttachment: true, neighborType: .freeform, neighborSpacing: .default)))
}
}
@ -879,7 +892,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
allowFullWidth = true
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId == firstMessage.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == firstMessage.id {
isBroadcastChannel = true
}
@ -906,7 +919,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let isFailed = item.content.firstMessage.effectivelyFailed(timestamp: item.context.account.network.getApproximateRemoteTimestamp())
var needShareButton = false
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
needShareButton = false
allowFullWidth = true
} else if isFailed || Namespaces.Message.allScheduled.contains(item.message.id.namespace) {
@ -1053,7 +1066,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
} else if let attribute = attribute as? ReplyMarkupMessageAttribute, attribute.flags.contains(.inline), !attribute.rows.isEmpty && !isPreview {
replyMarkup = attribute
} else if let attribute = attribute as? AuthorSignatureMessageAttribute {
if firstMessage.author is TelegramChannel, !attribute.signature.isEmpty {
if let chatPeer = firstMessage.peers[firstMessage.id.peerId] as? TelegramChannel, case .group = chatPeer.info, firstMessage.author is TelegramChannel, !attribute.signature.isEmpty {
authorRank = .custom(attribute.signature)
}
}
@ -1115,8 +1128,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform)
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
if index != 0 {
topBubbleAttributes = contentPropertiesAndPrepareLayouts[index - 1].3
}
@ -1124,8 +1137,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
bottomBubbleAttributes = contentPropertiesAndPrepareLayouts[index + 1].3
}
topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType)
bottomPosition = .Neighbour(bottomBubbleAttributes.isAttachment, bottomBubbleAttributes.neighborType)
topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType, topBubbleAttributes.neighborSpacing)
bottomPosition = .Neighbour(bottomBubbleAttributes.isAttachment, bottomBubbleAttributes.neighborType, bottomBubbleAttributes.neighborSpacing)
let prepareContentPosition: ChatMessageBubblePreparePosition
if let mosaicRange = mosaicRange, mosaicRange.contains(index) {
@ -1145,23 +1158,21 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let contentItem = ChatMessageBubbleContentItem(context: item.context, controllerInteraction: item.controllerInteraction, message: message, read: read, chatLocation: item.chatLocation, presentationData: item.presentationData, associatedData: item.associatedData, attributes: attributes)
var itemSelection: Bool?
if case .mosaic = prepareContentPosition {
switch content {
case .message:
break
case let .group(messages):
for (m, _, selection, _) in messages {
if m.id == message.id {
switch selection {
case .none:
break
case let .selectable(selected):
itemSelection = selected
}
break
switch content {
case .message:
break
case let .group(messages):
for (m, _, selection, _) in messages {
if m.id == message.id {
switch selection {
case .none:
break
case let .selectable(selected):
itemSelection = selected
}
break
}
}
}
}
let (properties, unboundSize, maxNodeWidth, nodeLayout) = prepareLayout(contentItem, layoutConstants, prepareContentPosition, itemSelection, CGSize(width: maximumContentWidth, height: CGFloat.greatestFiniteMagnitude))
@ -1261,7 +1272,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let firstNodeTopPosition: ChatMessageBubbleRelativePosition
if displayHeader {
firstNodeTopPosition = .Neighbour(false, .freeform)
firstNodeTopPosition = .Neighbour(false, .freeform, .default)
} else {
firstNodeTopPosition = .None(topNodeMergeStatus)
}
@ -1555,7 +1566,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
if mosaicRange.upperBound - 1 == contentNodeCount - 1 {
lastMosaicBottomPosition = lastNodeTopPosition
} else {
lastMosaicBottomPosition = .Neighbour(false, .freeform)
lastMosaicBottomPosition = .Neighbour(false, .freeform, .default)
}
if position.contains(.bottom), case .Neighbour = lastMosaicBottomPosition {
@ -1618,8 +1629,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
let topPosition: ChatMessageBubbleRelativePosition
let bottomPosition: ChatMessageBubbleRelativePosition
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform)
var topBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
var bottomBubbleAttributes = BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)
if i != 0 {
topBubbleAttributes = contentPropertiesAndLayouts[i - 1].3
}
@ -1630,19 +1641,19 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
if i == 0 {
topPosition = firstNodeTopPosition
} else {
topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType)
topPosition = .Neighbour(topBubbleAttributes.isAttachment, topBubbleAttributes.neighborType, topBubbleAttributes.neighborSpacing)
}
if i == contentNodeCount - 1 {
bottomPosition = lastNodeTopPosition
} else {
bottomPosition = .Neighbour(bottomBubbleAttributes.isAttachment, bottomBubbleAttributes.neighborType)
bottomPosition = .Neighbour(bottomBubbleAttributes.isAttachment, bottomBubbleAttributes.neighborType, bottomBubbleAttributes.neighborSpacing)
}
contentPosition = .linear(top: topPosition, bottom: bottomPosition)
case .mosaic:
assertionFailure()
contentPosition = .linear(top: .Neighbour(false, .freeform), bottom: .Neighbour(false, .freeform))
contentPosition = .linear(top: .Neighbour(false, .freeform, .default), bottom: .Neighbour(false, .freeform, .default))
}
let (contentNodeWidth, contentNodeFinalize) = contentNodeLayout(CGSize(width: maximumNodeWidth, height: CGFloat.greatestFiniteMagnitude), contentPosition)
#if DEBUG
@ -2774,7 +2785,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
}
private func traceSelectionNodes(parent: ASDisplayNode, point: CGPoint) -> ASDisplayNode? {
if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) {
if let parent = parent as? FileMessageSelectionNode, parent.bounds.contains(point) {
return parent
} else if let parent = parent as? GridMessageSelectionNode, parent.bounds.contains(point) {
return parent
} else {
if let parentSubnodes = parent.subnodes {
@ -2955,7 +2968,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePrevewItemNode
default:
break
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
canHaveSelection = false
}

View File

@ -106,7 +106,7 @@ final class ChatMessageCommentFooterContentNode: ChatMessageBubbleContentNode {
let displaySeparator: Bool
let topOffset: CGFloat
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media) = top {
if case let .linear(top, _) = preparePosition, case .Neighbour(_, .media, _) = top {
displaySeparator = false
topOffset = 2.0
} else {

View File

@ -176,7 +176,7 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .BubbleIncoming
} else {

View File

@ -34,19 +34,21 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
self.addSubnode(self.interactiveFileNode)
self.interactiveFileNode.toggleSelection = { [weak self] value in
if let strongSelf = self, let item = strongSelf.item {
item.controllerInteraction.toggleMessagesSelection([item.message.id], value)
}
}
self.interactiveFileNode.activateLocalContent = { [weak self] in
if let strongSelf = self {
if let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
}
self.interactiveFileNode.requestUpdateLayout = { [weak self] _ in
if let strongSelf = self {
if let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
if let strongSelf = self, let item = strongSelf.item {
let _ = item.controllerInteraction.requestMessageUpdate(item.message.id)
}
}
}
@ -58,7 +60,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let interactiveFileLayout = self.interactiveFileNode.asyncLayout()
return { item, layoutConstants, preparePosition, _, constrainedSize in
return { item, layoutConstants, preparePosition, selection, constrainedSize in
var selectedFile: TelegramMediaFile?
for media in item.message.media {
if let telegramFile = media as? TelegramMediaFile {
@ -69,7 +71,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
let statusType: ChatMessageDateAndStatusType?
switch preparePosition {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {
@ -87,7 +89,7 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
let automaticDownload = shouldDownloadMediaAutomatically(settings: item.controllerInteraction.automaticMediaDownloadSettings, peerType: item.associatedData.automaticDownloadPeerType, networkType: item.associatedData.automaticDownloadNetworkType, authorPeerId: item.message.author?.id, contactsPeerIds: item.associatedData.contactsPeerIds, media: selectedFile!)
let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.chatLocation, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
let (initialWidth, refineLayout) = interactiveFileLayout(item.context, item.presentationData, item.message, item.chatLocation, item.attributes, selectedFile!, automaticDownload, item.message.effectivelyIncoming(item.context.account.peerId), item.associatedData.isRecentActions, item.associatedData.forcedResourceStatus, statusType, item.message.groupingKey != nil ? selection : nil, CGSize(width: constrainedSize.width - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right, height: constrainedSize.height))
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: false, headerSpacing: 0.0, hidesBackground: .never, forceFullCorners: false, forceAlignment: .none)
@ -97,7 +99,15 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
return (refinedWidth + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, { boundingWidth in
let (fileSize, fileApply) = finishLayout(boundingWidth - layoutConstants.file.bubbleInsets.left - layoutConstants.file.bubbleInsets.right)
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + layoutConstants.file.bubbleInsets.bottom), { [weak self] _, synchronousLoads in
var bottomInset = layoutConstants.file.bubbleInsets.bottom
if case let .linear(_, bottom) = position {
if case .Neighbour(_, _, .condensed) = bottom {
bottomInset -= 24.0
}
}
return (CGSize(width: fileSize.width + layoutConstants.file.bubbleInsets.left + layoutConstants.file.bubbleInsets.right, height: fileSize.height + layoutConstants.file.bubbleInsets.top + bottomInset), { [weak self] _, synchronousLoads in
if let strongSelf = self {
strongSelf.item = item

View File

@ -194,7 +194,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id {
isBroadcastChannel = true
}
@ -473,7 +473,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
strongSelf.addSubnode(updatedShareButtonNode)
updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside)
}
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, message: item.message, account: item.context.account)
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, message: item.message, account: item.context.account)
updatedShareButtonNode.frame = CGRect(origin: CGPoint(x: videoFrame.maxX - 7.0, y: videoFrame.maxY - 24.0 - buttonSize.height), size: buttonSize)
} else if let shareButtonNode = strongSelf.shareButtonNode {
shareButtonNode.removeFromSupernode()
@ -858,7 +858,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
return
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
return
}

View File

@ -14,6 +14,7 @@ import TelegramStringFormatting
import RadialStatusNode
import SemanticStatusNode
import FileMediaResourceStatus
import CheckNode
private struct FetchControls {
let fetch: () -> Void
@ -21,6 +22,10 @@ private struct FetchControls {
}
final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var selectionBackgroundNode: ASDisplayNode?
private var selectionNode: FileMessageSelectionNode?
private var cutoutNode: ASDisplayNode?
private let titleNode: TextNode
private let descriptionNode: TextNode
private let descriptionMeasuringNode: TextNode
@ -35,7 +40,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var iconNode: TransformImageNode?
private var statusNode: SemanticStatusNode?
private var playbackAudioLevelView: VoiceBlobView?
private var streamingStatusNode: RadialStatusNode?
private var streamingStatusNode: SemanticStatusNode?
private var tapRecognizer: UITapGestureRecognizer?
private let statusDisposable = MetaDisposable()
@ -76,6 +81,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var actualFetchStatus: MediaResourceStatus?
private let fetchDisposable = MetaDisposable()
var toggleSelection: (Bool) -> Void = { _ in }
var activateLocalContent: () -> Void = { }
var requestUpdateLayout: (Bool) -> Void = { _ in }
@ -86,8 +92,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
private var progressFrame: CGRect?
private var streamingCacheStatusFrame: CGRect?
private var fileIconImage: UIImage?
private var cloudFetchIconImage: UIImage?
private var cloudFetchedIconImage: UIImage?
override init() {
self.titleNode = TextNode()
@ -204,7 +208,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
func asyncLayout() -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void))) {
let currentFile = self.file
let titleAsyncLayout = TextNode.asyncLayout(self.titleNode)
@ -214,7 +218,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
let currentMessage = self.message
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
return (CGFloat.greatestFiniteMagnitude, { constrainedSize in
let titleFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 16.0 / 17.0))
let descriptionFont = Font.regular(floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0))
@ -277,7 +281,11 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
var consumableContentIcon: UIImage?
for attribute in message.attributes {
if let attribute = attribute as? ConsumableContentMessageAttribute {
if !attribute.consumed {
var isConsumed = attribute.consumed
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info {
isConsumed = true
}
if !isConsumed {
if incoming {
consumableContentIcon = PresentationResourcesChat.chatBubbleConsumableContentIncomingIcon(presentationData.theme.theme)
} else {
@ -411,15 +419,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
textConstrainedSize.width -= 80.0
}
let streamingProgressDiameter: CGFloat = 28.0
var hasStreamingProgress = false
if isAudio && !isVoice {
hasStreamingProgress = true
if hasStreamingProgress {
textConstrainedSize.width -= streamingProgressDiameter + 4.0
}
}
let streamingProgressDiameter: CGFloat = 20.0
let (titleLayout, titleApply) = titleAsyncLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: hasThumbnail ? 2 : 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (descriptionLayout, descriptionApply) = descriptionAsyncLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
@ -457,15 +457,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
if let statusSize = statusSize {
minLayoutWidth = max(minLayoutWidth, statusSize.width)
}
var cloudFetchIconImage: UIImage?
var cloudFetchedIconImage: UIImage?
if hasStreamingProgress {
minLayoutWidth += streamingProgressDiameter + 4.0
cloudFetchIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchOutgoingIcon(presentationData.theme.theme)
cloudFetchedIconImage = incoming ? PresentationResourcesChat.chatBubbleFileCloudFetchedIncomingIcon(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleFileCloudFetchedOutgoingIcon(presentationData.theme.theme)
}
let fileIconImage: UIImage?
if hasThumbnail {
fileIconImage = nil
@ -547,10 +539,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
if isAudio && !isVoice {
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - streamingProgressDiameter + 1.0, y: 8.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
if hasStreamingProgress {
fittedLayoutSize.width += streamingProgressDiameter + 6.0
}
streamingCacheStatusFrame = CGRect(origin: CGPoint(x: progressFrame.maxX - streamingProgressDiameter + 2.0, y: progressFrame.maxY - streamingProgressDiameter + 2.0), size: CGSize(width: streamingProgressDiameter, height: streamingProgressDiameter))
fittedLayoutSize.width = max(fittedLayoutSize.width, boundingWidth + 2.0)
} else {
streamingCacheStatusFrame = CGRect()
@ -684,7 +673,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}))
}
strongSelf.waveformNode.displaysAsynchronously = !presentationData.isPreview
strongSelf.statusNode?.displaysAsynchronously = !presentationData.isPreview
strongSelf.statusNode?.frame = progressFrame
@ -692,8 +681,6 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
strongSelf.progressFrame = progressFrame
strongSelf.streamingCacheStatusFrame = streamingCacheStatusFrame
strongSelf.fileIconImage = fileIconImage
strongSelf.cloudFetchIconImage = cloudFetchIconImage
strongSelf.cloudFetchedIconImage = cloudFetchedIconImage
if let updatedFetchControls = updatedFetchControls {
let _ = strongSelf.fetchControls.swap(updatedFetchControls)
@ -702,7 +689,64 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
strongSelf.updateStatus(animated: !synchronousLoads)
let isAnimated = !synchronousLoads
let transition: ContainedViewLayoutTransition = isAnimated ? .animated(duration: 0.2, curve: .spring) : .immediate
if let selection = messageSelection {
if let streamingStatusNode = strongSelf.streamingStatusNode {
transition.updateAlpha(node: streamingStatusNode, alpha: 0.0)
transition.updateTransformScale(node: streamingStatusNode, scale: 0.2)
}
let selectionFrame = CGRect(origin: CGPoint(), size: fittedLayoutSize)
if let selectionNode = strongSelf.selectionNode {
selectionNode.frame = selectionFrame
selectionNode.updateSelected(selection, animated: isAnimated)
} else {
let selectionNode = FileMessageSelectionNode(theme: presentationData.theme.theme, incoming: incoming, toggle: { [weak self] value in
self?.toggleSelection(value)
})
strongSelf.selectionNode = selectionNode
strongSelf.addSubnode(selectionNode)
selectionNode.frame = selectionFrame
selectionNode.updateSelected(selection, animated: false)
if isAnimated {
selectionNode.animateIn()
}
}
let selectionBackgroundFrame = CGRect(origin: CGPoint(x: -8.0, y: -9.0), size: CGSize(width: fittedLayoutSize.width + 16.0, height: fittedLayoutSize.height + 6.0))
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
selectionBackgroundNode.frame = selectionBackgroundFrame
selectionBackgroundNode.isHidden = !selection
} else {
let selectionBackgroundNode = ASDisplayNode()
selectionBackgroundNode.backgroundColor = messageTheme.accentControlColor.withAlphaComponent(0.08)
selectionBackgroundNode.frame = selectionBackgroundFrame
selectionBackgroundNode.isHidden = !selection
strongSelf.selectionBackgroundNode = selectionBackgroundNode
strongSelf.insertSubnode(selectionBackgroundNode, at: 0)
}
} else {
if let streamingStatusNode = strongSelf.streamingStatusNode {
transition.updateAlpha(node: streamingStatusNode, alpha: 1.0)
transition.updateTransformScale(node: streamingStatusNode, scale: 1.0)
}
if let selectionNode = strongSelf.selectionNode {
strongSelf.selectionNode = nil
if isAnimated {
selectionNode.animateOut(completion: { [weak selectionNode] in
selectionNode?.removeFromSupernode()
})
} else {
selectionNode.removeFromSupernode()
}
}
if let selectionBackgroundNode = strongSelf.selectionBackgroundNode {
strongSelf.selectionBackgroundNode = nil
selectionBackgroundNode.removeFromSupernode()
}
}
strongSelf.updateStatus(animated: isAnimated)
}
})
})
@ -747,7 +791,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
let state: SemanticStatusNodeState
var streamingState: RadialStatusNodeState = .none
var streamingState: SemanticStatusNodeState = .none
let isSending = message.flags.isSending
@ -791,35 +835,19 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
if isAudio && !isVoice && !isSending {
let streamingStatusForegroundColor: UIColor = messageTheme.accentControlColor
let streamingStatusBackgroundColor: UIColor = messageTheme.mediaInactiveControlColor
switch resourceStatus.fetchStatus {
case let .Fetching(_, progress):
let adjustedProgress = max(progress, 0.027)
streamingState = .cloudProgress(color: streamingStatusForegroundColor, strokeBackgroundColor: streamingStatusBackgroundColor, lineWidth: 2.0, value: CGFloat(adjustedProgress))
streamingState = .progress(value: CGFloat(adjustedProgress), cancelEnabled: true, appearance: .init(inset: 1.0, lineWidth: 2.0))
case .Local:
if let cloudFetchedIconImage = self.cloudFetchedIconImage {
streamingState = .customIcon(cloudFetchedIconImage)
} else {
streamingState = .none
}
streamingState = .none
case .Remote:
if let cloudFetchIconImage = self.cloudFetchIconImage {
streamingState = .customIcon(cloudFetchIconImage)
} else {
streamingState = .none
}
streamingState = .download
}
} else {
streamingState = .none
}
let statusForegroundColor: UIColor
if self.iconNode != nil {
statusForegroundColor = presentationData.theme.theme.chat.message.mediaOverlayControlColors.foregroundColor
} else {
statusForegroundColor = incoming ? presentationData.theme.theme.chat.message.incoming.mediaControlInnerBackgroundColor : presentationData.theme.theme.chat.message.outgoing.mediaControlInnerBackgroundColor
}
switch resourceStatus.mediaStatus {
case var .fetchStatus(fetchStatus):
if self.message?.forwardInfo != nil {
@ -899,7 +927,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.playbackAudioLevelView?.setColor(presentationData.theme.theme.chat.inputPanel.actionControlFillColor)
if streamingState != .none && self.streamingStatusNode == nil {
let streamingStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
let streamingStatusNode = SemanticStatusNode(backgroundNodeColor: backgroundNodeColor, foregroundNodeColor: foregroundNodeColor)
self.streamingStatusNode = streamingStatusNode
streamingStatusNode.frame = streamingCacheStatusFrame
self.addSubnode(streamingStatusNode)
@ -940,6 +968,36 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
}
}
if streamingState == .none && self.selectionNode == nil {
if let cutoutNode = self.cutoutNode {
self.cutoutNode = nil
if animated {
cutoutNode.layer.animateScale(from: 1.0, to: 0.001, duration: 0.2, removeOnCompletion: false) { [weak cutoutNode] _ in
cutoutNode?.removeFromSupernode()
}
} else {
cutoutNode.removeFromSupernode()
}
}
} else if let statusNode = self.statusNode {
if let _ = self.cutoutNode {
} else {
let cutoutNode = ASImageNode()
cutoutNode.displaysAsynchronously = false
cutoutNode.displayWithoutProcessing = true
cutoutNode.image = generateFilledCircleImage(diameter: 23.0, color: messageTheme.bubble.withWallpaper.fill)
self.cutoutNode = cutoutNode
self.insertSubnode(cutoutNode, aboveSubnode: statusNode)
cutoutNode.frame = streamingCacheStatusFrame.insetBy(dx: -1.5, dy: -1.5)
if animated {
cutoutNode.layer.animateScale(from: 0.001, to: 1.0, duration: 0.2)
}
}
}
if let (expandedString, compactString, font) = downloadingStrings {
self.fetchingTextNode.attributedText = NSAttributedString(string: expandedString, font: font, textColor: messageTheme.fileDurationColor)
self.fetchingCompactTextNode.attributedText = NSAttributedString(string: compactString, font: font, textColor: messageTheme.fileDurationColor)
@ -971,12 +1029,12 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
self.fetchingCompactTextNode.frame = CGRect(origin: self.descriptionNode.frame.origin, size: fetchingCompactSize)
}
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
static func asyncLayout(_ node: ChatMessageInteractiveFileNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))) {
let currentAsyncLayout = node?.asyncLayout()
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize in
return { context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize in
var fileNode: ChatMessageInteractiveFileNode
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
var fileLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ chatLocation: ChatLocation, _ attributes: ChatMessageEntryAttributes, _ file: TelegramMediaFile, _ automaticDownload: Bool, _ incoming: Bool, _ isRecentActions: Bool, _ forcedResourceStatus: FileMediaResourceStatus?, _ dateAndStatusType: ChatMessageDateAndStatusType?, _ messageSelection: Bool?, _ constrainedSize: CGSize) -> (CGFloat, (CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> Void)))
if let node = node, let currentAsyncLayout = currentAsyncLayout {
fileNode = node
@ -986,7 +1044,7 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
fileLayout = fileNode.asyncLayout()
}
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, constrainedSize)
let (initialWidth, continueLayout) = fileLayout(context, presentationData, message, chatLocation, attributes, file, automaticDownload, incoming, isRecentActions, forcedResourceStatus, dateAndStatusType, messageSelection, constrainedSize)
return (initialWidth, { constrainedSize in
let (finalWidth, finalLayout) = continueLayout(constrainedSize)
@ -1049,3 +1107,60 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode {
return nil
}
}
final class FileMessageSelectionNode: ASDisplayNode {
private let toggle: (Bool) -> Void
private var selected = false
private let checkNode: CheckNode
public init(theme: PresentationTheme, incoming: Bool, toggle: @escaping (Bool) -> Void) {
self.toggle = toggle
self.checkNode = CheckNode(strokeColor: incoming ? theme.chat.message.incoming.mediaPlaceholderColor : theme.chat.message.outgoing.mediaPlaceholderColor, fillColor: theme.list.itemCheckColors.fillColor, foregroundColor: theme.list.itemCheckColors.foregroundColor, style: .compact)
self.checkNode.isUserInteractionEnabled = false
super.init()
self.addSubnode(self.checkNode)
}
override public func didLoad() {
super.didLoad()
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
public func animateIn() {
self.checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
self.checkNode.layer.animateScale(from: 0.2, to: 1.0, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
}
public func animateOut(completion: @escaping () -> Void) {
self.checkNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.checkNode.layer.animateScale(from: 1.0, to: 0.2, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
completion()
})
}
public func updateSelected(_ selected: Bool, animated: Bool) {
if self.selected != selected {
self.selected = selected
self.checkNode.setIsChecked(selected, animated: animated)
}
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
self.toggle(!self.selected)
}
}
override public func layout() {
super.layout()
let checkSize = CGSize(width: 30.0, height: 30.0)
self.checkNode.frame = CGRect(origin: CGPoint(x: 23.0, y: 17.0), size: checkSize)
}
}

View File

@ -327,7 +327,7 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
if let peer = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = peer.info {
isBroadcastChannel = true
}
} else if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.messageId == message.id {
} else if case let .replyThread(replyThreadMessage) = chatLocation, replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == message.id {
isBroadcastChannel = true
}
if !hasActionMedia && !isBroadcastChannel {

View File

@ -157,7 +157,7 @@ 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))
relativePosition = .linear(top: top, bottom: .Neighbour(false, .freeform, .default))
}
imageCorners = chatMessageBubbleImageContentCorners(relativeContentPosition: relativePosition, normalRadius: layoutConstants.image.defaultCornerRadius, mergedRadius: layoutConstants.image.mergedCornerRadius, mergedWithAnotherContentRadius: layoutConstants.image.contentMergedCornerRadius, layoutConstants: layoutConstants, chatPresentationData: item.presentationData)
@ -212,7 +212,7 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if selectedMedia?.venue != nil || activeLiveBroadcastingTimeout != nil {
if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .BubbleIncoming

View File

@ -207,7 +207,7 @@ class ChatMessageMediaBubbleContentNode: ChatMessageBubbleContentNode {
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if item.message.effectivelyIncoming(item.context.account.peerId) {
statusType = .ImageIncoming
} else {

View File

@ -1054,7 +1054,7 @@ class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {

View File

@ -85,7 +85,7 @@ class ChatMessageRestrictedBubbleContentNode: ChatMessageBubbleContentNode {
let statusType: ChatMessageDateAndStatusType?
switch position {
case .linear(_, .None), .linear(_, .Neighbour(true, _)):
case .linear(_, .None), .linear(_, .Neighbour(true, _, _)):
if incoming {
statusType = .BubbleIncoming
} else {

View File

@ -257,7 +257,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
isBroadcastChannel = true
}
if replyThreadMessage.isChannelPost, replyThreadMessage.messageId == item.message.id {
if replyThreadMessage.isChannelPost, replyThreadMessage.effectiveTopId == item.message.id {
isBroadcastChannel = true
}
@ -568,7 +568,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
strongSelf.addSubnode(updatedShareButtonNode)
updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside)
}
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, message: item.message, account: item.context.account)
let buttonSize = updatedShareButtonNode.update(presentationData: item.presentationData, chatLocation: item.chatLocation, message: item.message, account: item.context.account)
let shareButtonFrame = CGRect(origin: CGPoint(x: baseShareButtonFrame.minX, y: baseShareButtonFrame.maxY - buttonSize.height), size: buttonSize)
transition.updateFrame(node: updatedShareButtonNode, frame: shareButtonFrame)
} else if let shareButtonNode = strongSelf.shareButtonNode {
@ -922,7 +922,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
return
}
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.messageId == item.message.id {
if case let .replyThread(replyThreadMessage) = item.chatLocation, replyThreadMessage.effectiveTopId == item.message.id {
return
}

View File

@ -143,7 +143,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode {
case let .linear(_, neighbor):
if case .None = neighbor {
displayStatus = true
} else if case .Neighbour(true, _) = neighbor {
} else if case .Neighbour(true, _, _) = neighbor {
displayStatus = true
}
default:

View File

@ -13,10 +13,18 @@ import StickerResources
import PhotoResources
import TelegramStringFormatting
private enum PinnedMessageAnimation {
case slideToTop
case slideToBottom
}
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let context: AccountContext
private let tapButton: HighlightTrackingButtonNode
private let closeButton: HighlightableButtonNode
private let clippingContainer: ASDisplayNode
private let contentContainer: ASDisplayNode
private let lineNode: ASImageNode
private let titleNode: TextNode
private let textNode: TextNode
@ -25,7 +33,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
private let separatorNode: ASDisplayNode
private var currentLayout: (CGFloat, CGFloat, CGFloat)?
private var currentMessage: Message?
private var currentMessage: ChatPinnedMessage?
private var previousMediaReference: AnyMediaReference?
private var isReplyThread: Bool = false
@ -46,6 +54,11 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.separatorNode = ASDisplayNode()
self.separatorNode.isLayerBacked = true
self.clippingContainer = ASDisplayNode()
self.clippingContainer.clipsToBounds = true
self.contentContainer = ASDisplayNode()
self.lineNode = ASImageNode()
self.lineNode.displayWithoutProcessing = true
self.lineNode.displaysAsynchronously = false
@ -87,10 +100,12 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside])
self.addSubnode(self.closeButton)
self.addSubnode(self.clippingContainer)
self.clippingContainer.addSubnode(self.contentContainer)
self.addSubnode(self.lineNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
self.addSubnode(self.imageNode)
self.contentContainer.addSubnode(self.titleNode)
self.contentContainer.addSubnode(self.textNode)
self.contentContainer.addSubnode(self.imageNode)
self.tapButton.addTarget(self, action: #selector(self.tapped), forControlEvents: [.touchUpInside])
self.addSubnode(self.tapButton)
@ -128,10 +143,18 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.closeButton.isHidden = isReplyThread
var messageUpdated = false
var messageUpdatedAnimation: PinnedMessageAnimation?
if let currentMessage = self.currentMessage, let pinnedMessage = interfaceState.pinnedMessage {
if currentMessage.id != pinnedMessage.id || currentMessage.stableVersion != pinnedMessage.stableVersion {
if currentMessage != pinnedMessage {
messageUpdated = true
}
if currentMessage.message.id != pinnedMessage.message.id {
if currentMessage.message.id < pinnedMessage.message.id {
messageUpdatedAnimation = .slideToTop
} else {
messageUpdatedAnimation = .slideToBottom
}
}
} else if (self.currentMessage != nil) != (interfaceState.pinnedMessage != nil) {
messageUpdated = true
}
@ -141,7 +164,7 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
self.currentMessage = interfaceState.pinnedMessage
if let currentMessage = currentMessage, let currentLayout = self.currentLayout {
self.enqueueTransition(width: currentLayout.0, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
self.enqueueTransition(width: currentLayout.0, leftInset: currentLayout.1, rightInset: currentLayout.2, transition: .immediate, animation: messageUpdatedAnimation, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: self.context.account.peerId, firstTime: previousMessageWasNil, isReplyThread: isReplyThread)
}
}
@ -156,18 +179,43 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight - UIScreenPixel), size: CGSize(width: width, height: UIScreenPixel)))
self.tapButton.frame = CGRect(origin: CGPoint(), size: CGSize(width: width - rightInset - closeButtonSize.width - 4.0, height: panelHeight))
self.clippingContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
self.contentContainer.frame = CGRect(origin: CGPoint(), size: CGSize(width: width, height: panelHeight))
if self.currentLayout?.0 != width || self.currentLayout?.1 != leftInset || self.currentLayout?.2 != rightInset {
self.currentLayout = (width, leftInset, rightInset)
if let currentMessage = self.currentMessage {
self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, message: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
self.enqueueTransition(width: width, leftInset: leftInset, rightInset: rightInset, transition: .immediate, animation: .none, pinnedMessage: currentMessage, theme: interfaceState.theme, strings: interfaceState.strings, nameDisplayOrder: interfaceState.nameDisplayOrder, accountPeerId: interfaceState.accountPeerId, firstTime: true, isReplyThread: isReplyThread)
}
}
return panelHeight
}
private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, message: Message, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
private func enqueueTransition(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition, animation: PinnedMessageAnimation?, pinnedMessage: ChatPinnedMessage, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, accountPeerId: PeerId, firstTime: Bool, isReplyThread: Bool) {
let message = pinnedMessage.message
if let animation = animation {
let offset: CGFloat
switch animation {
case .slideToTop:
offset = -40.0
case .slideToBottom:
offset = 40.0
}
if let copyView = self.contentContainer.view.snapshotView(afterScreenUpdates: false) {
copyView.frame = self.contentContainer.frame
self.clippingContainer.view.addSubview(copyView)
copyView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: offset), duration: 0.2, removeOnCompletion: false, additive: true)
copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak copyView] _ in
copyView?.removeFromSuperview()
})
self.contentContainer.layer.animatePosition(from: CGPoint(x: 0.0, y: -offset), to: CGPoint(), duration: 0.2, additive: true)
self.contentContainer.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeTextLayout = TextNode.asyncLayout(self.textNode)
let imageNodeLayout = self.imageNode.asyncLayout()
@ -191,7 +239,12 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
var updatedMediaReference: AnyMediaReference?
var imageDimensions: CGSize?
var titleString = strings.Conversation_PinnedMessage
var titleString: String
if pinnedMessage.isLatest {
titleString = strings.Conversation_PinnedMessage
} else {
titleString = strings.Conversation_PinnedPreviousMessage
}
for media in message.media {
if let image = media as? TelegramMediaImage {
@ -300,13 +353,8 @@ final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
if let interfaceInteraction = self.interfaceInteraction, let message = self.currentMessage {
if self.isReplyThread {
interfaceInteraction.scrollToTop()
/*if let sourceReference = message.sourceReference {
interfaceInteraction.navigateToMessage(sourceReference.messageId, true)
} else {
interfaceInteraction.navigateToMessage(message.id, false)
}*/
} else {
interfaceInteraction.navigateToMessage(message.id, false)
interfaceInteraction.navigateToMessage(message.message.id, false)
}
}
}

View File

@ -257,6 +257,32 @@ struct ChatSlowmodeState: Equatable {
var variant: ChatSlowmodeVariant
}
final class ChatPinnedMessage: Equatable {
let message: Message
let isLatest: Bool
init(message: Message, isLatest: Bool) {
self.message = message
self.isLatest = isLatest
}
static func ==(lhs: ChatPinnedMessage, rhs: ChatPinnedMessage) -> Bool {
if lhs === rhs {
return true
}
if lhs.message.id != rhs.message.id {
return false
}
if lhs.message.stableVersion != rhs.message.stableVersion {
return false
}
if lhs.isLatest != rhs.isLatest {
return false
}
return true
}
}
final class ChatPresentationInterfaceState: Equatable {
let interfaceState: ChatInterfaceState
let chatLocation: ChatLocation
@ -274,7 +300,7 @@ final class ChatPresentationInterfaceState: Equatable {
let titlePanelContexts: [ChatTitlePanelContext]
let keyboardButtonsMessage: Message?
let pinnedMessageId: MessageId?
let pinnedMessage: Message?
let pinnedMessage: ChatPinnedMessage?
let peerIsBlocked: Bool
let peerIsMuted: Bool
let peerDiscussionId: PeerId?
@ -348,7 +374,7 @@ final class ChatPresentationInterfaceState: Equatable {
self.peerNearbyData = peerNearbyData
}
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: Message?, 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?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, isScheduledMessages: Bool, peerNearbyData: ChatPeerNearbyData?) {
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?, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, limitsConfiguration: LimitsConfiguration, fontSize: PresentationFontSize, bubbleCorners: PresentationChatBubbleCorners, accountPeerId: PeerId, mode: ChatControllerPresentationMode, hasScheduledMessages: Bool, isScheduledMessages: Bool, peerNearbyData: ChatPeerNearbyData?) {
self.interfaceState = interfaceState
self.chatLocation = chatLocation
self.renderedPeer = renderedPeer
@ -447,14 +473,7 @@ final class ChatPresentationInterfaceState: Equatable {
if lhs.pinnedMessageId != rhs.pinnedMessageId {
return false
}
if let lhsMessage = lhs.pinnedMessage, let rhsMessage = rhs.pinnedMessage {
if lhsMessage.id != rhsMessage.id {
return false
}
if lhsMessage.stableVersion != rhsMessage.stableVersion {
return false
}
} else if (lhs.pinnedMessage != nil) != (rhs.pinnedMessage != nil) {
if lhs.pinnedMessage != rhs.pinnedMessage {
return false
}
if lhs.callsAvailable != rhs.callsAvailable {
@ -613,7 +632,7 @@ 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: 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: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, 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, isScheduledMessages: self.isScheduledMessages, peerNearbyData: self.peerNearbyData)
}
func updatedPinnedMessage(_ pinnedMessage: Message?) -> ChatPresentationInterfaceState {
func updatedPinnedMessage(_ pinnedMessage: ChatPinnedMessage?) -> 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: 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: self.editingUrlPreview, search: self.search, searchQuerySuggestionResult: self.searchQuerySuggestionResult, 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, isScheduledMessages: self.isScheduledMessages, peerNearbyData: self.peerNearbyData)
}

View File

@ -453,6 +453,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode {
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: self.automaticMediaDownloadSettings,

View File

@ -189,9 +189,6 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode {
canSearchMembers = false
}
}
if case .replyThread = interfaceState.chatLocation {
canSearchMembers = false
}
self.membersButton.isHidden = (!(interfaceState.search?.query.isEmpty ?? true)) || self.displayActivity || !canSearchMembers
let resultsEnabled = (resultCount ?? 0) > 0

View File

@ -456,7 +456,12 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
var accessibilityText = ""
for segment in self.titleNode.segments {
accessibilityText.append(segment.attributedText.string)
switch segment {
case let .number(_, string):
accessibilityText.append(string.string)
case let .text(_, string):
accessibilityText.append(string.string)
}
}
self.accessibilityLabel = accessibilityText

View File

@ -13,6 +13,7 @@ import PresentationDataUtils
import SearchUI
import TelegramPermissionsUI
import AppBundle
import DeviceAccess
public class ComposeController: ViewController {
private let context: AccountContext
@ -183,6 +184,42 @@ public class ComposeController: ViewController {
}
}
self.contactsNode.openCreateContact = { [weak self] in
let _ = (DeviceAccess.authorizationStatus(subject: .contacts)
|> take(1)
|> deliverOnMainQueue).start(next: { status in
guard let strongSelf = self else {
return
}
switch status {
case .allowed:
let contactData = DeviceContactExtendedData(basicData: DeviceContactBasicData(firstName: "", lastName: "", phoneNumbers: [DeviceContactPhoneNumberData(label: "_$!<Mobile>!$_", value: "+")]), middleName: "", prefix: "", suffix: "", organization: "", jobTitle: "", department: "", emailAddresses: [], urls: [], addresses: [], birthdayDate: nil, socialProfiles: [], instantMessagingProfiles: [], note: "")
(strongSelf.navigationController as? NavigationController)?.pushViewController(strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .create(peer: nil, contactData: contactData, isSharing: false, shareViaException: false, completion: { peer, stableId, contactData in
guard let strongSelf = self else {
return
}
if let peer = peer {
DispatchQueue.main.async {
if let navigationController = strongSelf.navigationController as? NavigationController {
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id)))
}
}
} else {
(strongSelf.navigationController as? NavigationController)?.replaceAllButRootController(strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .vcard(nil, stableId, contactData), completed: nil, cancelled: nil), animated: true)
}
}), completed: nil, cancelled: nil))
case .notDetermined:
DeviceAccess.authorizeAccess(to: .contacts)
default:
let presentationData = strongSelf.presentationData
strongSelf.present(textAlertController(context: strongSelf.context, title: presentationData.strings.AccessDenied_Title, text: presentationData.strings.Contacts_AccessDeniedError, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_NotNow, action: {}), TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
self?.context.sharedContext.applicationBindings.openSettings()
})]), in: .window(.root))
}
})
}
self.contactsNode.openCreateNewChannel = { [weak self] in
if let strongSelf = self {
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }

View File

@ -27,6 +27,7 @@ final class ComposeControllerNode: ASDisplayNode {
var openCreateNewGroup: (() -> Void)?
var openCreateNewSecretChat: (() -> Void)?
var openCreateContact: (() -> Void)?
var openCreateNewChannel: (() -> Void)?
private var presentationData: PresentationData
@ -39,14 +40,15 @@ final class ComposeControllerNode: ASDisplayNode {
var openCreateNewGroupImpl: (() -> Void)?
var openCreateNewSecretChatImpl: (() -> Void)?
var openCreateContactImpl: (() -> Void)?
var openCreateNewChannelImpl: (() -> Void)?
self.contactListNode = ContactListNode(context: context, presentation: .single(.natural(options: [
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewGroup, icon: .generic(UIImage(bundleImageName: "Contact List/CreateGroupActionIcon")!), action: {
openCreateNewGroupImpl?()
}),
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewEncryptedChat, icon: .generic(UIImage(bundleImageName: "Contact List/CreateSecretChatActionIcon")!), action: {
openCreateNewSecretChatImpl?()
ContactListAdditionalOption(title: self.presentationData.strings.NewContact_Title, icon: .generic(UIImage(bundleImageName: "Contact List/AddMemberIcon")!), action: {
openCreateContactImpl?()
}),
ContactListAdditionalOption(title: self.presentationData.strings.Compose_NewChannel, icon: .generic(UIImage(bundleImageName: "Contact List/CreateChannelActionIcon")!), action: {
openCreateNewChannelImpl?()
@ -69,6 +71,10 @@ final class ComposeControllerNode: ASDisplayNode {
openCreateNewSecretChatImpl = { [weak self] in
self?.openCreateNewSecretChat?()
}
openCreateContactImpl = { [weak self] in
self?.contactListNode.listNode.clearHighlightAnimated(true)
self?.openCreateContact?()
}
openCreateNewChannelImpl = { [weak self] in
self?.openCreateNewChannel?()
}

View File

@ -146,6 +146,7 @@ private final class DrawingStickersScreenNode: ViewControllerTracingNode {
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -6,6 +6,7 @@ import SyncCore
import SwiftSignalKit
import Display
import Pdf
import AVFoundation
public struct ICloudFileResourceId: MediaResourceId {
public let urlData: String
@ -68,9 +69,16 @@ public class ICloudFileResource: TelegramMediaResource {
}
struct ICloudFileDescription {
struct AudioMetadata {
let title: String?
let performer: String?
let duration: Int
}
let urlData: String
let fileName: String
let fileSize: Int
let audioMetadata: AudioMetadata?
}
private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? {
@ -91,7 +99,18 @@ private func descriptionWithUrl(_ url: URL) -> ICloudFileDescription? {
return nil
}
let result = ICloudFileDescription(urlData: urlData.base64EncodedString(), fileName: fileName, fileSize: fileSize)
var audioMetadata: ICloudFileDescription.AudioMetadata?
if ["mp3", "m4a"].contains(url.pathExtension.lowercased()) {
let asset = AVURLAsset(url: url)
let title = AVMetadataItem.metadataItems(from: asset.commonMetadata, withKey: AVMetadataKey.commonKeyTitle, keySpace: AVMetadataKeySpace.common).first?.stringValue
let performer = AVMetadataItem.metadataItems(from: asset.commonMetadata, withKey: AVMetadataKey.commonKeyArtist, keySpace: AVMetadataKeySpace.common).first?.stringValue
let duration = CMTimeGetSeconds(asset.duration)
if duration > 0 {
audioMetadata = ICloudFileDescription.AudioMetadata(title: title, performer: performer, duration: Int(duration))
}
}
let result = ICloudFileDescription(urlData: urlData.base64EncodedString(), fileName: fileName, fileSize: fileSize, audioMetadata: audioMetadata)
url.stopAccessingSecurityScopedResource()

View File

@ -136,6 +136,7 @@ final class OverlayAudioPlayerControllerNode: ViewControllerTracingNode, UIGestu
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings, pollActionState: ChatInterfacePollActionState(), stickerSettings: ChatInterfaceStickerSettings(loopAnimatedStickers: false))

View File

@ -1961,6 +1961,7 @@ private final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewD
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -1200,6 +1200,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
}, openPeerContextMenu: { _, _, _, _ in
}, openMessageReplies: { _, _, _ in
}, openReplyThreadOriginalMessage: { _ in
}, openMessageStats: { _ in
}, requestMessageUpdate: { _ in
}, cancelInteractiveKeyboardGestures: {
}, automaticMediaDownloadSettings: MediaAutoDownloadSettings.defaultSettings,

View File

@ -75,18 +75,24 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
let impl: WebEmbedImplementation
private let openUrl: (URL) -> Void
private let intrinsicDimensions: CGSize
private let webView: WKWebView
private let semaphore = DispatchSemaphore(value: 0)
private let queue = Queue()
init(impl: WebEmbedImplementation, intrinsicDimensions: CGSize) {
init(impl: WebEmbedImplementation, intrinsicDimensions: CGSize, openUrl: @escaping (URL) -> Void) {
self.impl = impl
self.intrinsicDimensions = intrinsicDimensions
self.openUrl = openUrl
let userContentController = WKUserContentController()
userContentController.addUserScript(WKUserScript(source: "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta)", injectionTime: .atDocumentEnd, forMainFrameOnly: true))
if impl is YoutubeEmbedImplementation {
} else {
userContentController.addUserScript(WKUserScript(source: "var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta)", injectionTime: .atDocumentEnd, forMainFrameOnly: true))
}
let configuration = WKWebViewConfiguration()
configuration.allowsInlineMediaPlayback = true
@ -112,6 +118,8 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
self.webView.navigationDelegate = self
self.webView.scrollView.isScrollEnabled = false
self.webView.allowsLinkPreview = false
self.webView.allowsBackForwardNavigationGestures = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.webView.accessibilityIgnoresInvertColors = true
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
@ -159,19 +167,19 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
}
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
print("w")
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
self.impl.pageReady()
}
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
if let error = error as? WKError, error.code.rawValue == 204 {
return
}
}
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if let url = navigationAction.request.url, url.scheme == "embed" {
self.impl.callback(url: url)
@ -179,6 +187,9 @@ final class WebEmbedPlayerNode: ASDisplayNode, WKNavigationDelegate {
} else if let _ = navigationAction.targetFrame {
decisionHandler(.allow)
} else {
if let url = navigationAction.request.url, url.absoluteString.contains("youtube") {
self.openUrl(url)
}
decisionHandler(.cancel)
}
}

View File

@ -19,8 +19,9 @@ public final class WebEmbedVideoContent: UniversalVideoContent {
public let dimensions: CGSize
public let duration: Int32
let forcedTimestamp: Int?
let openUrl: (URL) -> Void
public init?(webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil) {
public init?(webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) {
guard let embedUrl = webpageContent.embedUrl else {
return nil
}
@ -30,10 +31,11 @@ public final class WebEmbedVideoContent: UniversalVideoContent {
self.dimensions = webpageContent.embedSize?.cgSize ?? CGSize(width: 128.0, height: 128.0)
self.duration = Int32(webpageContent.duration ?? (0 as Int))
self.forcedTimestamp = forcedTimestamp
self.openUrl = openUrl
}
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp)
return WebEmbedVideoContentNode(postbox: postbox, audioSessionManager: audioSession, webPage: self.webPage, webpageContent: self.webpageContent, forcedTimestamp: self.forcedTimestamp, openUrl: self.openUrl)
}
}
@ -70,7 +72,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
private var readyDisposable = MetaDisposable()
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil) {
init(postbox: Postbox, audioSessionManager: ManagedAudioSession, webPage: TelegramMediaWebpage, webpageContent: TelegramMediaWebpageLoadedContent, forcedTimestamp: Int? = nil, openUrl: @escaping (URL) -> Void) {
self.webpageContent = webpageContent
if let embedSize = webpageContent.embedSize {
@ -83,7 +85,7 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
let embedType = webEmbedType(content: webpageContent, forcedTimestamp: forcedTimestamp)
let embedImpl = webEmbedImplementation(for: embedType)
self.playerNode = WebEmbedPlayerNode(impl: embedImpl, intrinsicDimensions: self.intrinsicDimensions)
self.playerNode = WebEmbedPlayerNode(impl: embedImpl, intrinsicDimensions: self.intrinsicDimensions, openUrl: openUrl)
super.init()

View File

@ -174,8 +174,8 @@ final class YoutubeEmbedImplementation: WebEmbedImplementation {
updateStatus(self.status)
let html = String(format: htmlTemplate, paramsJson)
webView.loadHTMLString(html, baseURL: URL(string: "https://youtube.com/"))
webView.isUserInteractionEnabled = false
webView.loadHTMLString(html, baseURL: URL(string: "https://messenger.telegram.org"))
// webView.isUserInteractionEnabled = false
userContentController.addUserScript(WKUserScript(source: userScript, injectionTime: .atDocumentEnd, forMainFrameOnly: false))
}

View File

@ -64,6 +64,7 @@ public struct ChannelMemberListState {
enum ChannelMemberListCategory {
case recent
case recentSearch(String)
case mentions(MessageId?, String?)
case admins(String?)
case contacts(String?)
case bots(String?)
@ -211,6 +212,12 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
requestCategory = .recent(.all)
case let .recentSearch(query):
requestCategory = .recent(.search(query))
case let .mentions(threadId, query):
if let query = query, !query.isEmpty {
requestCategory = .mentions(threadId: threadId, filter: .search(query))
} else {
requestCategory = .mentions(threadId: threadId, filter: .all)
}
case let .admins(query):
requestCategory = .admins
adminQuery = query
@ -521,6 +528,8 @@ private final class ChannelMemberSingleCategoryListContext: ChannelMemberCategor
}
}
}
case .mentions:
break
}
}
if updatedList {
@ -728,7 +737,7 @@ final class PeerChannelMemberCategoriesContext {
emptyTimeout = 0.0
}
switch key {
case .recent, .recentSearch, .admins, .contacts, .bots:
case .recent, .recentSearch, .admins, .contacts, .bots, .mentions:
let mappedCategory: ChannelMemberListCategory
switch key {
case .recent:
@ -741,6 +750,8 @@ final class PeerChannelMemberCategoriesContext {
mappedCategory = .contacts(query)
case let .bots(query):
mappedCategory = .bots(query)
case let .mentions(threadId, query):
mappedCategory = .mentions(threadId, query)
default:
mappedCategory = .recent
}

View File

@ -8,6 +8,7 @@ import TelegramStringFormatting
enum PeerChannelMemberContextKey: Equatable, Hashable {
case recent
case recentSearch(String)
case mentions(threadId: MessageId?, query: String?)
case admins(String?)
case contacts(String?)
case bots(String?)
@ -321,6 +322,11 @@ public final class PeerChannelMemberCategoriesContextsManager {
return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated)
}
public func mentions(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, threadMessageId: MessageId?, searchQuery: String? = nil, requestUpdate: Bool = true, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) {
let key: PeerChannelMemberContextKey = .mentions(threadId: threadMessageId, query: searchQuery)
return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: key, requestUpdate: requestUpdate, updated: updated)
}
public func admins(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, searchQuery: String? = nil, updated: @escaping (ChannelMemberListState) -> Void) -> (Disposable, PeerChannelMemberCategoryControl?) {
return self.getContext(postbox: postbox, network: network, accountPeerId: accountPeerId, peerId: peerId, key: .admins(searchQuery), requestUpdate: true, updated: updated)
}

View File

@ -18,6 +18,7 @@ static_library(
"tgcalls/tgcalls/platform/darwin/VideoCameraCapturerMac.*",
"tgcalls/tgcalls/platform/darwin/VideoMetalViewMac.*",
"tgcalls/tgcalls/platform/darwin/GLVideoViewMac.*",
"tgcalls/tgcalls/platform/darwin/ScreenCapturer.*",
]),
has_cpp = True,
headers = merge_maps([

View File

@ -19,6 +19,7 @@ objc_library(
"tgcalls/tgcalls/platform/darwin/VideoCameraCapturerMac.*",
"tgcalls/tgcalls/platform/darwin/VideoMetalViewMac.*",
"tgcalls/tgcalls/platform/darwin/GLVideoViewMac.*",
"tgcalls/tgcalls/platform/darwin/ScreenCapturer.*",
]),
hdrs = glob([
"PublicHeaders/**/*.h",

@ -1 +1 @@
Subproject commit b906b5a955f1e6e195cb91c4513d4cb7b3620ea6
Subproject commit 64f96a1b4fcfb8afdb0fb7749082cb42cdad7901

View File

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