Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios

This commit is contained in:
Ilya Laktyushin 2023-06-30 20:31:49 +02:00
commit f502b22990
56 changed files with 1140 additions and 363 deletions

View File

@ -9373,3 +9373,8 @@ Sorry for the inconvenience.";
"Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**"; "Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**";
"Conversation.StoryForwardTooltip.ManyChats.One" = "Story forwarded to to **%@** and %@ others"; "Conversation.StoryForwardTooltip.ManyChats.One" = "Story forwarded to to **%@** and %@ others";
"Conversation.StoryForwardTooltip.SavedMessages.One" = "Story forwarded to to **Saved Messages**"; "Conversation.StoryForwardTooltip.SavedMessages.One" = "Story forwarded to to **Saved Messages**";
"Conversation.StoryMentionTextOutgoing" = "You mentioned %@\nin a story";
"Conversation.StoryMentionTextIncoming" = "%@ mentioned you\nin a story";
"Conversation.StoryExpiredMentionTextOutgoing" = "The story where you mentioned %@\n is no longer available";
"Conversation.StoryExpiredMentionTextIncoming" = "The story you were mentioned in\nis no longer available";

View File

@ -871,6 +871,7 @@ public protocol SharedAccountContext: AnyObject {
func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController func makeAttachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentFileController
func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject? func makeGalleryCaptionPanelView(context: AccountContext, chatLocation: ChatLocation, customEmojiAvailable: Bool, present: @escaping (ViewController) -> Void, presentInGlobalOverlay: @escaping (ViewController) -> Void) -> NSObject?
func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String) -> ViewController func makeHashtagSearchController(context: AccountContext, peer: EnginePeer?, query: String) -> ViewController
func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams) func navigateToChatController(_ params: NavigateToChatControllerParams)
func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController) func navigateToForumChannel(context: AccountContext, peerId: EnginePeer.Id, navigationController: NavigationController)
func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError> func navigateToForumThread(context: AccountContext, peerId: EnginePeer.Id, threadId: Int64, messageId: EngineMessage.Id?, navigationController: NavigationController, activateInput: ChatControllerActivateInput?, keepStack: NavigateToChatKeepStack) -> Signal<Never, NoError>

View File

@ -49,8 +49,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let topicAuthorId: EnginePeer.Id? public let topicAuthorId: EnginePeer.Id?
public let hasBots: Bool public let hasBots: Bool
public let translateToLanguage: String? public let translateToLanguage: String?
public let maxReadStoryId: Int32?
public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false), topicAuthorId: EnginePeer.Id? = nil, hasBots: Bool = false, translateToLanguage: String? = nil) { public init(automaticDownloadPeerType: MediaAutoDownloadPeerType, automaticDownloadPeerId: EnginePeer.Id?, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, isRecentActions: Bool = false, subject: ChatControllerSubject? = nil, contactsPeerIds: Set<EnginePeer.Id> = Set(), channelDiscussionGroup: ChannelDiscussionGroupStatus = .unknown, animatedEmojiStickers: [String: [StickerPackItem]] = [:], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]] = [:], forcedResourceStatus: FileMediaResourceStatus? = nil, currentlyPlayingMessageId: EngineMessage.Index? = nil, isCopyProtectionEnabled: Bool = false, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, accountPeer: EnginePeer?, forceInlineReactions: Bool = false, alwaysDisplayTranscribeButton: DisplayTranscribeButton = DisplayTranscribeButton(canBeDisplayed: false, displayForNotConsumed: false), topicAuthorId: EnginePeer.Id? = nil, hasBots: Bool = false, translateToLanguage: String? = nil, maxReadStoryId: Int32? = nil) {
self.automaticDownloadPeerType = automaticDownloadPeerType self.automaticDownloadPeerType = automaticDownloadPeerType
self.automaticDownloadPeerId = automaticDownloadPeerId self.automaticDownloadPeerId = automaticDownloadPeerId
self.automaticDownloadNetworkType = automaticDownloadNetworkType self.automaticDownloadNetworkType = automaticDownloadNetworkType
@ -72,6 +73,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton
self.hasBots = hasBots self.hasBots = hasBots
self.translateToLanguage = translateToLanguage self.translateToLanguage = translateToLanguage
self.maxReadStoryId = maxReadStoryId
} }
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool { public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -135,6 +137,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.translateToLanguage != rhs.translateToLanguage { if lhs.translateToLanguage != rhs.translateToLanguage {
return false return false
} }
if lhs.maxReadStoryId != rhs.maxReadStoryId {
return false
}
return true return true
} }
} }

View File

@ -97,6 +97,7 @@ swift_library(
"//submodules/TelegramUI/Components/Stories/StoryPeerListComponent", "//submodules/TelegramUI/Components/Stories/StoryPeerListComponent",
"//submodules/TelegramUI/Components/FullScreenEffectView", "//submodules/TelegramUI/Components/FullScreenEffectView",
"//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent",
"//submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -47,6 +47,7 @@ import InviteLinksUI
import ChatFolderLinkPreviewScreen import ChatFolderLinkPreviewScreen
import StoryContainerScreen import StoryContainerScreen
import FullScreenEffectView import FullScreenEffectView
import PeerInfoStoryGridScreen
private final class ContextControllerContentSourceImpl: ContextControllerContentSource { private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController let controller: ViewController
@ -2604,6 +2605,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
self.openStoryCamera() self.openStoryCamera()
}) })
}))) })))
items.append(.action(ContextMenuActionItem(text: "Saved Stories", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
self.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .saved))
})
})))
items.append(.action(ContextMenuActionItem(text: "Archived Stories", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor)
}, action: { [weak self] c, _ in
c.dismiss(completion: {
guard let self else {
return
}
self.push(PeerInfoStoryGridScreen(context: self.context, peerId: self.context.account.peerId, scope: .archive))
})
})))
} else { } else {
items.append(.action(ContextMenuActionItem(text: "Send Message", icon: { theme in items.append(.action(ContextMenuActionItem(text: "Send Message", icon: { theme in
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor) return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/MessageBubble"), color: theme.contextMenu.primaryColor)

View File

@ -295,9 +295,17 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
messageText = "📊 \(poll.text)" messageText = "📊 \(poll.text)"
case let dice as TelegramMediaDice: case let dice as TelegramMediaDice:
messageText = dice.emoji messageText = dice.emoji
case _ as TelegramMediaStory: case let story as TelegramMediaStory:
//TODO:localize if story.isMention, let peer {
messageText = "Story" if message.flags.contains(.Incoming) {
messageText = strings.Conversation_StoryMentionTextIncoming(peer.compactDisplayTitle).string
} else {
messageText = strings.Conversation_StoryMentionTextOutgoing(peer.compactDisplayTitle).string
}
} else {
//TODO:localize
messageText = "Story"
}
default: default:
break break
} }

View File

@ -21,7 +21,7 @@ open class TransformImageNode: ASDisplayNode {
private var disposable = MetaDisposable() private var disposable = MetaDisposable()
private var currentTransform: ((TransformImageArguments) -> DrawingContext?)? private var currentTransform: ((TransformImageArguments) -> DrawingContext?)?
private var currentArguments: TransformImageArguments? public private(set) var currentArguments: TransformImageArguments?
public private(set) var image: UIImage? public private(set) var image: UIImage?
private var argumentsPromise = ValuePromise<TransformImageArguments>(ignoreRepeated: true) private var argumentsPromise = ValuePromise<TransformImageArguments>(ignoreRepeated: true)

View File

@ -952,8 +952,8 @@ public func chatMessagePhotoThumbnail(account: Account, userLocation: MediaResou
} }
} }
public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { public func chatMessageVideoThumbnail(account: Account, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, blurred: Bool = false, synchronousLoads: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred) let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred)
return signal return signal
|> map { value in |> map { value in

View File

@ -2615,6 +2615,13 @@ final class MessageHistoryTable: Table {
} }
} }
} }
#if DEBUG
for key in associatedStories.keys {
associatedStories[key] = CodableEntry(data: Data())
}
#endif
associatedMessageIds.append(contentsOf: attribute.associatedMessageIds) associatedMessageIds.append(contentsOf: attribute.associatedMessageIds)
if addAssociatedMessages { if addAssociatedMessages {
for messageId in attribute.associatedMessageIds { for messageId in attribute.associatedMessageIds {

View File

@ -275,7 +275,7 @@ private func filteredGlobalSound(_ sound: PeerMessageSound) -> PeerMessageSound
} }
} }
private func notificationsPeerCategoryEntries(category: NotificationsPeerCategory, globalSettings: GlobalNotificationSettingsSet, state: NotificationExceptionState, presentationData: PresentationData, notificationSoundList: NotificationSoundList?) -> [NotificationsPeerCategoryEntry] { private func notificationsPeerCategoryEntries(category: NotificationsPeerCategory, globalSettings: GlobalNotificationSettingsSet, state: NotificationExceptionState, presentationData: PresentationData, notificationSoundList: NotificationSoundList?, automaticTopPeers: [EnginePeer], automaticNotificationSettings: [EnginePeer.Id: EnginePeer.NotificationSettings]) -> [NotificationsPeerCategoryEntry] {
var entries: [NotificationsPeerCategoryEntry] = [] var entries: [NotificationsPeerCategoryEntry] = []
let notificationSettings: MessageNotificationSettings let notificationSettings: MessageNotificationSettings
@ -333,7 +333,7 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased())) entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased()))
entries.append(.addException(presentationData.theme, presentationData.strings.Notification_Exceptions_AddException)) entries.append(.addException(presentationData.theme, presentationData.strings.Notification_Exceptions_AddException))
let sortedExceptions = notificationExceptions.settings.sorted(by: { lhs, rhs in var sortedExceptions = notificationExceptions.settings.sorted(by: { lhs, rhs in
let lhsName = lhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let lhsName = lhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
let rhsName = rhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder) let rhsName = rhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
@ -355,99 +355,114 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
return lhsName < rhsName return lhsName < rhsName
}) })
var automaticSet = Set<EnginePeer.Id>()
if globalSettings.privateChats.storySettings.mute == .default {
for peer in automaticTopPeers {
if sortedExceptions.contains(where: { $0.key == peer.id }) {
continue
}
sortedExceptions.append((peer.id, NotificationExceptionWrapper(settings: automaticNotificationSettings[peer.id]?._asNotificationSettings() ?? .defaultSettings, peer: peer, date: nil)))
automaticSet.insert(peer.id)
}
}
var existingPeerIds = Set<EnginePeer.Id>() var existingPeerIds = Set<EnginePeer.Id>()
var index: Int = 0 var index: Int = 0
for (_, value) in sortedExceptions { for (_, value) in sortedExceptions {
if !value.peer.isDeleted { if !value.peer.isDeleted {
var title: String var title: String = ""
if case .stories = category { if automaticSet.contains(value.peer.id) {
var muted = false //TODO:localize
if value.settings.storySettings.mute == .muted { title = "\(presentationData.strings.Notification_Exceptions_AlwaysOn) (automatic)"
muted = true
title = presentationData.strings.Notification_Exceptions_AlwaysOff
} else {
title = presentationData.strings.Notification_Exceptions_AlwaysOn
}
if !muted {
switch value.settings.storySettings.sound {
case .default:
break
default:
if !title.isEmpty {
title.append(", ")
}
title.append(presentationData.strings.Notification_Exceptions_SoundCustom)
}
switch value.settings.storySettings.hideSender {
case .default:
break
default:
if !title.isEmpty {
title += ", "
}
//TODO:localize
if case .show = value.settings.displayPreviews {
title += "Show Names"
} else {
title += "Hide Names"
}
}
}
} else { } else {
var muted = false if case .stories = category {
switch value.settings.muteState { var muted = false
case let .muted(until): if value.settings.storySettings.mute == .muted {
if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) { muted = true
if until < Int32.max - 1 { title.append(presentationData.strings.Notification_Exceptions_AlwaysOff)
let formatter = DateFormatter()
formatter.locale = Locale(identifier: presentationData.strings.baseLanguageCode)
if Calendar.current.isDateInToday(Date(timeIntervalSince1970: Double(until))) {
formatter.dateFormat = "HH:mm"
} else {
formatter.dateFormat = "E, d MMM HH:mm"
}
let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(until)))
title = presentationData.strings.Notification_Exceptions_MutedUntil(dateString).string
} else {
muted = true
title = presentationData.strings.Notification_Exceptions_AlwaysOff
}
} else { } else {
title = presentationData.strings.Notification_Exceptions_AlwaysOn title.append(presentationData.strings.Notification_Exceptions_AlwaysOn)
} }
case .unmuted:
title = presentationData.strings.Notification_Exceptions_AlwaysOn if !muted {
default: switch value.settings.storySettings.sound {
title = "" case .default:
} break
if !muted { default:
switch value.settings.messageSound { if !title.isEmpty {
case .default: title.append(", ")
break }
default: title.append(presentationData.strings.Notification_Exceptions_SoundCustom)
if !title.isEmpty { }
title.append(", ") switch value.settings.storySettings.hideSender {
case .default:
break
default:
if !title.isEmpty {
title += ", "
}
//TODO:localize
if case .show = value.settings.displayPreviews {
title += "Show Names"
} else {
title += "Hide Names"
}
} }
title.append(presentationData.strings.Notification_Exceptions_SoundCustom)
} }
switch value.settings.displayPreviews { } else {
case .default: var muted = false
break switch value.settings.muteState {
default: case let .muted(until):
if !title.isEmpty { if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
title += ", " if until < Int32.max - 1 {
} let formatter = DateFormatter()
if case .show = value.settings.displayPreviews { formatter.locale = Locale(identifier: presentationData.strings.baseLanguageCode)
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn
if Calendar.current.isDateInToday(Date(timeIntervalSince1970: Double(until))) {
formatter.dateFormat = "HH:mm"
} else {
formatter.dateFormat = "E, d MMM HH:mm"
}
let dateString = formatter.string(from: Date(timeIntervalSince1970: Double(until)))
title = presentationData.strings.Notification_Exceptions_MutedUntil(dateString).string
} else {
muted = true
title = presentationData.strings.Notification_Exceptions_AlwaysOff
}
} else { } else {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff title = presentationData.strings.Notification_Exceptions_AlwaysOn
}
case .unmuted:
title = presentationData.strings.Notification_Exceptions_AlwaysOn
default:
title = ""
}
if !muted {
switch value.settings.messageSound {
case .default:
break
default:
if !title.isEmpty {
title.append(", ")
}
title.append(presentationData.strings.Notification_Exceptions_SoundCustom)
}
switch value.settings.displayPreviews {
case .default:
break
default:
if !title.isEmpty {
title += ", "
}
if case .show = value.settings.displayPreviews {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOn
} else {
title += presentationData.strings.Notification_Exceptions_PreviewAlwaysOff
}
} }
} }
} }
@ -923,8 +938,35 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings]) let sharedData = context.sharedContext.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.inAppNotificationSettings])
let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications]) let preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get()) var automaticData: Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> = .single(([], [:]))
|> map { presentationData, notificationSoundList, sharedData, view, state -> (ItemListControllerState, (ItemListNodeState, Any)) in if case .stories = category {
automaticData = context.engine.peers.recentPeers()
|> mapToSignal { recentPeers -> Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> in
guard case let .peers(peersValue) = recentPeers else {
return .single(([], [:]))
}
let peers = peersValue.prefix(5).map(EnginePeer.init)
return context.engine.data.subscribe(
EngineDataMap(peers.map { peer in
return TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peer.id)
})
)
|> map { settings -> ([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]) in
var settingsMap: [EnginePeer.Id: EnginePeer.NotificationSettings] = [:]
for peer in peers {
if let value = settings[peer.id] {
settingsMap[peer.id] = value
} else {
settingsMap[peer.id] = EnginePeer.NotificationSettings(TelegramPeerNotificationSettings.defaultSettings)
}
}
return (peers, settingsMap)
}
}
}
let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get(), automaticData)
|> map { presentationData, notificationSoundList, sharedData, view, state, automaticData -> (ItemListControllerState, (ItemListNodeState, Any)) in
let viewSettings: GlobalNotificationSettingsSet let viewSettings: GlobalNotificationSettingsSet
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) { if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective viewSettings = settings.effective
@ -932,7 +974,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
viewSettings = GlobalNotificationSettingsSet.defaultSettings viewSettings = GlobalNotificationSettingsSet.defaultSettings
} }
let entries = notificationsPeerCategoryEntries(category: category, globalSettings: viewSettings, state: state, presentationData: presentationData, notificationSoundList: notificationSoundList) let entries = notificationsPeerCategoryEntries(category: category, globalSettings: viewSettings, state: state, presentationData: presentationData, notificationSoundList: notificationSoundList, automaticTopPeers: automaticData.0, automaticNotificationSettings: automaticData.1)
var index = 0 var index = 0
var scrollToItem: ListViewScrollToItem? var scrollToItem: ListViewScrollToItem?

View File

@ -23,6 +23,7 @@ public protocol SparseItemGridView: UIView {
public protocol SparseItemGridDisplayItem: AnyObject { public protocol SparseItemGridDisplayItem: AnyObject {
var layer: SparseItemGridLayer? { get } var layer: SparseItemGridLayer? { get }
var view: SparseItemGridView? { get } var view: SparseItemGridView? { get }
var blurLayer: SimpleLayer? { get }
} }
public protocol SparseItemGridShimmerLayer: CALayer { public protocol SparseItemGridShimmerLayer: CALayer {
@ -342,6 +343,7 @@ public final class SparseItemGrid: ASDisplayNode {
let layer: SparseItemGridLayer? let layer: SparseItemGridLayer?
let view: SparseItemGridView? let view: SparseItemGridView?
var shimmerLayer: SparseItemGridShimmerLayer? var shimmerLayer: SparseItemGridShimmerLayer?
var blurLayer: SimpleLayer?
init(layer: SparseItemGridLayer?, view: SparseItemGridView?) { init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
self.layer = layer self.layer = layer
@ -391,8 +393,9 @@ public final class SparseItemGrid: ASDisplayNode {
let itemSpacing: CGFloat let itemSpacing: CGFloat
let lastItemSize: CGFloat let lastItemSize: CGFloat
let itemsPerRow: Int let itemsPerRow: Int
let centerItems: Bool
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) { init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel, itemCount: Int) {
self.containerLayout = containerLayout self.containerLayout = containerLayout
let width: CGFloat let width: CGFloat
if containerLayout.useSideInsets { if containerLayout.useSideInsets {
@ -400,15 +403,23 @@ public final class SparseItemGrid: ASDisplayNode {
} else { } else {
width = containerLayout.size.width width = containerLayout.size.width
} }
var centerItems = false
if let fixedItemHeight = containerLayout.fixedItemHeight { if let fixedItemHeight = containerLayout.fixedItemHeight {
self.itemsPerRow = 1 self.itemsPerRow = 1
self.itemSize = CGSize(width: width, height: fixedItemHeight) self.itemSize = CGSize(width: width, height: fixedItemHeight)
self.lastItemSize = width self.lastItemSize = width
self.itemSpacing = 0.0 self.itemSpacing = 0.0
self.centerItems = false
} else { } else {
self.itemSpacing = 1.0 self.itemSpacing = 1.0
let itemsPerRow = CGFloat(zoomLevel.rawValue) let itemsPerRow: CGFloat
if containerLayout.fixedItemAspect != nil && itemCount <= 2 {
itemsPerRow = 2.0
centerItems = itemCount == 1
} else {
itemsPerRow = CGFloat(zoomLevel.rawValue)
}
self.itemsPerRow = Int(itemsPerRow) self.itemsPerRow = Int(itemsPerRow)
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow) let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
if let fixedItemAspect = containerLayout.fixedItemAspect { if let fixedItemAspect = containerLayout.fixedItemAspect {
@ -417,7 +428,12 @@ public final class SparseItemGrid: ASDisplayNode {
self.itemSize = CGSize(width: itemSize, height: itemSize) self.itemSize = CGSize(width: itemSize, height: itemSize)
} }
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1) if centerItems {
self.lastItemSize = self.itemSize.width
} else {
self.lastItemSize = width - (self.itemSize.width + self.itemSpacing) * CGFloat(self.itemsPerRow - 1)
}
self.centerItems = centerItems
} }
} }
@ -425,8 +441,11 @@ public final class SparseItemGrid: ASDisplayNode {
let row = index / self.itemsPerRow let row = index / self.itemsPerRow
let column = index % self.itemsPerRow let column = index % self.itemsPerRow
var frame = CGRect(origin: CGPoint(x: (self.containerLayout.useSideInsets ? self.containerLayout.insets.left : 0.0) + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: self.containerLayout.insets.top + CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height))
return CGRect(origin: CGPoint(x: (self.containerLayout.useSideInsets ? self.containerLayout.insets.left : 0.0) + CGFloat(column) * (self.itemSize.width + self.itemSpacing), y: self.containerLayout.insets.top + CGFloat(row) * (self.itemSize.height + self.itemSpacing)), size: CGSize(width: column == (self.itemsPerRow - 1) ? self.lastItemSize : itemSize.width, height: itemSize.height)) if self.centerItems {
frame.origin.x = floor((self.containerLayout.size.width - frame.width) * 0.5)
}
return frame
} }
func contentHeight(count: Int) -> CGFloat { func contentHeight(count: Int) -> CGFloat {
@ -514,7 +533,7 @@ public final class SparseItemGrid: ASDisplayNode {
func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous) { func update(containerLayout: ContainerLayout, items: Items, restoreScrollPosition: (y: CGFloat, index: Int)?, synchronous: SparseItemGrid.Synchronous) {
if self.layout?.containerLayout != containerLayout || self.items !== items { if self.layout?.containerLayout != containerLayout || self.items !== items {
self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel) self.layout = Layout(containerLayout: containerLayout, zoomLevel: self.zoomLevel, itemCount: items.count)
self.items = items self.items = items
self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition) self.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition)
@ -950,6 +969,8 @@ public final class SparseItemGrid: ASDisplayNode {
var bindLayers: [SparseItemGridDisplayItem] = [] var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridDisplayItem] = [] var updateLayers: [SparseItemGridDisplayItem] = []
let addBlur = layout.centerItems
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count) let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
if visibleRange.maxIndex >= visibleRange.minIndex { if visibleRange.maxIndex >= visibleRange.minIndex {
for index in visibleRange.minIndex ... visibleRange.maxIndex { for index in visibleRange.minIndex ... visibleRange.maxIndex {
@ -974,6 +995,22 @@ public final class SparseItemGrid: ASDisplayNode {
} }
} }
if addBlur {
let blurLayer: SimpleLayer
if let current = itemLayer.blurLayer {
blurLayer = current
} else {
blurLayer = SimpleLayer()
blurLayer.masksToBounds = true
blurLayer.zPosition = -1.0
self.scrollView.layer.addSublayer(blurLayer)
itemLayer.blurLayer = blurLayer
}
} else if let blurLayer = itemLayer.blurLayer {
itemLayer.blurLayer = nil
blurLayer.removeFromSuperlayer()
}
if itemLayer.needsShimmer { if itemLayer.needsShimmer {
let placeholderLayer: SparseItemGridShimmerLayer let placeholderLayer: SparseItemGridShimmerLayer
if let current = itemLayer.shimmerLayer { if let current = itemLayer.shimmerLayer {
@ -995,6 +1032,9 @@ public final class SparseItemGrid: ASDisplayNode {
validIds.insert(item.id) validIds.insert(item.id)
itemLayer.frame = itemFrame itemLayer.frame = itemFrame
if let blurLayer = itemLayer.blurLayer {
blurLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: itemFrame.minY), size: CGSize(width: layout.containerLayout.size.width, height: itemFrame.height))
}
} else { } else {
let placeholderLayer: SparseItemGridShimmerLayer let placeholderLayer: SparseItemGridShimmerLayer
if self.visiblePlaceholders.count > usedPlaceholderCount { if self.visiblePlaceholders.count > usedPlaceholderCount {
@ -1022,7 +1062,7 @@ public final class SparseItemGrid: ASDisplayNode {
if let layer = item.layer { if let layer = item.layer {
layer.update(size: layer.frame.size) layer.update(size: layer.frame.size)
} else if let view = item.view { } else if let view = item.view {
view.update(size: layer.frame.size, insets: layout.containerLayout.insets) view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets)
} }
} }

View File

@ -935,7 +935,7 @@ fileprivate let parsers: [Int32 : (BufferReader) -> Any?] = {
dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) } dict[-1886646706] = { return Api.UrlAuthResult.parse_urlAuthResultAccepted($0) }
dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) } dict[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($0) }
dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) } dict[-1831650802] = { return Api.UrlAuthResult.parse_urlAuthResultRequest($0) }
dict[-1885878744] = { return Api.User.parse_user($0) } dict[-1414139616] = { return Api.User.parse_user($0) }
dict[-742634630] = { return Api.User.parse_userEmpty($0) } dict[-742634630] = { return Api.User.parse_userEmpty($0) }
dict[1340198022] = { return Api.UserFull.parse_userFull($0) } dict[1340198022] = { return Api.UserFull.parse_userFull($0) }
dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) } dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) }

View File

@ -452,14 +452,14 @@ public extension Api {
} }
public extension Api { public extension Api {
enum User: TypeConstructorDescription { enum User: TypeConstructorDescription {
case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?) case user(flags: Int32, flags2: Int32, id: Int64, accessHash: Int64?, firstName: String?, lastName: String?, username: String?, phone: String?, photo: Api.UserProfilePhoto?, status: Api.UserStatus?, botInfoVersion: Int32?, restrictionReason: [Api.RestrictionReason]?, botInlinePlaceholder: String?, langCode: String?, emojiStatus: Api.EmojiStatus?, usernames: [Api.Username]?, storiesMaxId: Int32?)
case userEmpty(id: Int64) case userEmpty(id: Int64)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) { public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
switch self { switch self {
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames): case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId):
if boxed { if boxed {
buffer.appendInt32(-1885878744) buffer.appendInt32(-1414139616)
} }
serializeInt32(flags, buffer: buffer, boxed: false) serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(flags2, buffer: buffer, boxed: false) serializeInt32(flags2, buffer: buffer, boxed: false)
@ -485,6 +485,7 @@ public extension Api {
for item in usernames! { for item in usernames! {
item.serialize(buffer, true) item.serialize(buffer, true)
}} }}
if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)}
break break
case .userEmpty(let id): case .userEmpty(let id):
if boxed { if boxed {
@ -497,8 +498,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) { public func descriptionFields() -> (String, [(String, Any)]) {
switch self { switch self {
case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames): case .user(let flags, let flags2, let id, let accessHash, let firstName, let lastName, let username, let phone, let photo, let status, let botInfoVersion, let restrictionReason, let botInlinePlaceholder, let langCode, let emojiStatus, let usernames, let storiesMaxId):
return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any)]) return ("user", [("flags", flags as Any), ("flags2", flags2 as Any), ("id", id as Any), ("accessHash", accessHash as Any), ("firstName", firstName as Any), ("lastName", lastName as Any), ("username", username as Any), ("phone", phone as Any), ("photo", photo as Any), ("status", status as Any), ("botInfoVersion", botInfoVersion as Any), ("restrictionReason", restrictionReason as Any), ("botInlinePlaceholder", botInlinePlaceholder as Any), ("langCode", langCode as Any), ("emojiStatus", emojiStatus as Any), ("usernames", usernames as Any), ("storiesMaxId", storiesMaxId as Any)])
case .userEmpty(let id): case .userEmpty(let id):
return ("userEmpty", [("id", id as Any)]) return ("userEmpty", [("id", id as Any)])
} }
@ -547,6 +548,8 @@ public extension Api {
if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() { if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
_16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self) _16 = Api.parseVector(reader, elementSignature: 0, elementType: Api.Username.self)
} } } }
var _17: Int32?
if Int(_2!) & Int(1 << 5) != 0 {_17 = reader.readInt32() }
let _c1 = _1 != nil let _c1 = _1 != nil
let _c2 = _2 != nil let _c2 = _2 != nil
let _c3 = _3 != nil let _c3 = _3 != nil
@ -563,8 +566,9 @@ public extension Api {
let _c14 = (Int(_1!) & Int(1 << 22) == 0) || _14 != nil let _c14 = (Int(_1!) & Int(1 << 22) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 30) == 0) || _15 != nil let _c15 = (Int(_1!) & Int(1 << 30) == 0) || _15 != nil
let _c16 = (Int(_2!) & Int(1 << 0) == 0) || _16 != nil let _c16 = (Int(_2!) & Int(1 << 0) == 0) || _16 != nil
if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 { let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil
return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16) if _c1 && _c2 && _c3 && _c4 && _c5 && _c6 && _c7 && _c8 && _c9 && _c10 && _c11 && _c12 && _c13 && _c14 && _c15 && _c16 && _c17 {
return Api.User.user(flags: _1!, flags2: _2!, id: _3!, accessHash: _4, firstName: _5, lastName: _6, username: _7, phone: _8, photo: _9, status: _10, botInfoVersion: _11, restrictionReason: _12, botInlinePlaceholder: _13, langCode: _14, emojiStatus: _15, usernames: _16, storiesMaxId: _17)
} }
else { else {
return nil return nil

View File

@ -8510,6 +8510,21 @@ public extension Api.functions.stories {
}) })
} }
} }
public extension Api.functions.stories {
static func getAllReadUserStories() -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.Updates>) {
let buffer = Buffer()
buffer.appendInt32(1922848300)
return (FunctionDescription(name: "stories.getAllReadUserStories", parameters: []), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in
let reader = BufferReader(buffer)
var result: Api.Updates?
if let signature = reader.readInt32() {
result = Api.parse(reader, signature: signature) as? Api.Updates
}
return result
})
}
}
public extension Api.functions.stories { public extension Api.functions.stories {
static func getAllStories(flags: Int32, state: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.AllStories>) { static func getAllStories(flags: Int32, state: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.AllStories>) {
let buffer = Buffer() let buffer = Buffer()

View File

@ -525,7 +525,7 @@ struct AccountMutableState {
var presences: [PeerId: Api.UserStatus] = [:] var presences: [PeerId: Api.UserStatus] = [:]
for user in users { for user in users {
switch user { switch user {
case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _): case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _):
if let status = status { if let status = status {
presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = status presences[PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))] = status
} }

View File

@ -380,8 +380,9 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
} }
case let .messageMediaDice(value, emoticon): case let .messageMediaDice(value, emoticon):
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil) return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil)
case let .messageMediaStory(_, userId, id, _): case let .messageMediaStory(flags, userId, id, _):
return (TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id)), nil, nil, nil) let isMention = (flags & (1 << 1)) != 0
return (TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id), isMention: isMention), nil, nil, nil)
} }
} }

View File

@ -46,7 +46,7 @@ func telegramMediaWebpageFromApiWebpage(_ webpage: Api.WebPage, url: String?) ->
webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute) webpageAttributes = attributes.compactMap(telegramMediaWebpageAttributeFromApiWebpageAttribute)
for attribute in attributes { for attribute in attributes {
if case let .webPageAttributeStory(_, userId, id, _) = attribute { if case let .webPageAttributeStory(_, userId, id, _) = attribute {
story = TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id)) story = TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id), isMention: false)
} }
} }
} }

View File

@ -36,7 +36,7 @@ extension TelegramPeerUsername {
extension TelegramUser { extension TelegramUser {
convenience init(user: Api.User) { convenience init(user: Api.User) {
switch user { switch user {
case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames): case let .user(flags, flags2, id, accessHash, firstName, lastName, username, phone, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, usernames, _):
let representations: [TelegramMediaImageRepresentation] = photo.flatMap(parsedTelegramProfilePhoto) ?? [] let representations: [TelegramMediaImageRepresentation] = photo.flatMap(parsedTelegramProfilePhoto) ?? []
let isMin = (flags & (1 << 20)) != 0 let isMin = (flags & (1 << 20)) != 0
@ -104,7 +104,7 @@ extension TelegramUser {
static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? { static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? {
switch rhs { switch rhs {
case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _): case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _, _):
let isMin = (flags & (1 << 20)) != 0 let isMin = (flags & (1 << 20)) != 0
if !isMin { if !isMin {
return TelegramUser(user: rhs) return TelegramUser(user: rhs)

View File

@ -23,7 +23,7 @@ extension TelegramUserPresence {
convenience init?(apiUser: Api.User) { convenience init?(apiUser: Api.User) {
switch apiUser { switch apiUser {
case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _): case let .user(_, _, _, _, _, _, _, _, _, status, _, _, _, _, _, _, _):
if let status = status { if let status = status {
self.init(apiStatus: status) self.init(apiStatus: status)
} else { } else {

View File

@ -192,7 +192,7 @@ extension Api.Chat {
extension Api.User { extension Api.User {
var peerId: PeerId { var peerId: PeerId {
switch self { switch self {
case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _): case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
case let .userEmpty(id): case let .userEmpty(id):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id)) return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))

View File

@ -7,19 +7,22 @@ public final class TelegramMediaStory: Media, Equatable {
public let peerIds: [PeerId] public let peerIds: [PeerId]
public let storyId: StoryId public let storyId: StoryId
public let isMention: Bool
public var storyIds: [StoryId] { public var storyIds: [StoryId] {
return [self.storyId] return [self.storyId]
} }
public init(storyId: StoryId) { public init(storyId: StoryId, isMention: Bool) {
self.storyId = storyId self.storyId = storyId
self.isMention = isMention
self.peerIds = [self.storyId.peerId] self.peerIds = [self.storyId.peerId]
} }
public init(decoder: PostboxDecoder) { public init(decoder: PostboxDecoder) {
self.storyId = StoryId(peerId: PeerId(decoder.decodeInt64ForKey("pid", orElse: 0)), id: decoder.decodeInt32ForKey("sid", orElse: 0)) self.storyId = StoryId(peerId: PeerId(decoder.decodeInt64ForKey("pid", orElse: 0)), id: decoder.decodeInt32ForKey("sid", orElse: 0))
self.isMention = decoder.decodeBoolForKey("mns", orElse: false)
self.peerIds = [self.storyId.peerId] self.peerIds = [self.storyId.peerId]
} }
@ -27,6 +30,7 @@ public final class TelegramMediaStory: Media, Equatable {
public func encode(_ encoder: PostboxEncoder) { public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.storyId.peerId.toInt64(), forKey: "pid") encoder.encodeInt64(self.storyId.peerId.toInt64(), forKey: "pid")
encoder.encodeInt32(self.storyId.id, forKey: "sid") encoder.encodeInt32(self.storyId.id, forKey: "sid")
encoder.encodeBool(self.isMention, forKey: "mns")
} }
public func isLikelyToBeUpdated() -> Bool { public func isLikelyToBeUpdated() -> Bool {
@ -48,6 +52,9 @@ public final class TelegramMediaStory: Media, Equatable {
if lhs.storyId != rhs.storyId { if lhs.storyId != rhs.storyId {
return false return false
} }
if lhs.isMention != rhs.isMention {
return false
}
return true return true
} }

View File

@ -133,6 +133,7 @@ public enum Stories {
case isPublic case isPublic
case isCloseFriends case isCloseFriends
case isForwardingDisabled case isForwardingDisabled
case isEdited
} }
public let id: Int32 public let id: Int32
@ -148,6 +149,7 @@ public enum Stories {
public let isPublic: Bool public let isPublic: Bool
public let isCloseFriends: Bool public let isCloseFriends: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: Bool
public init( public init(
id: Int32, id: Int32,
@ -162,7 +164,8 @@ public enum Stories {
isExpired: Bool, isExpired: Bool,
isPublic: Bool, isPublic: Bool,
isCloseFriends: Bool, isCloseFriends: Bool,
isForwardingDisabled: Bool isForwardingDisabled: Bool,
isEdited: Bool
) { ) {
self.id = id self.id = id
self.timestamp = timestamp self.timestamp = timestamp
@ -177,6 +180,7 @@ public enum Stories {
self.isPublic = isPublic self.isPublic = isPublic
self.isCloseFriends = isCloseFriends self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
} }
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
@ -201,6 +205,7 @@ public enum Stories {
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? false
self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false
self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false
} }
public func encode(to encoder: Encoder) throws { public func encode(to encoder: Encoder) throws {
@ -226,6 +231,7 @@ public enum Stories {
try container.encode(self.isPublic, forKey: .isPublic) try container.encode(self.isPublic, forKey: .isPublic)
try container.encode(self.isCloseFriends, forKey: .isCloseFriends) try container.encode(self.isCloseFriends, forKey: .isCloseFriends)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.isEdited, forKey: .isEdited)
} }
public static func ==(lhs: Item, rhs: Item) -> Bool { public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -276,6 +282,9 @@ public enum Stories {
if lhs.isForwardingDisabled != rhs.isForwardingDisabled { if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false return false
} }
if lhs.isEdited != rhs.isEdited {
return false
}
return true return true
} }
@ -870,7 +879,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)) items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp))
@ -1023,7 +1033,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
transaction.setStory(id: storyId, value: entry) transaction.setStory(id: storyId, value: entry)
@ -1046,7 +1057,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
@ -1178,7 +1190,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp) items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
@ -1200,7 +1213,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
updatedItems.append(updatedItem) updatedItems.append(updatedItem)
} }
@ -1297,6 +1311,7 @@ extension Stories.StoredItem {
let isPublic = (flags & (1 << 7)) != 0 let isPublic = (flags & (1 << 7)) != 0
let isCloseFriends = (flags & (1 << 8)) != 0 let isCloseFriends = (flags & (1 << 8)) != 0
let isForwardingDisabled = (flags & (1 << 10)) != 0 let isForwardingDisabled = (flags & (1 << 10)) != 0
let isEdited = (flags & (1 << 11)) != 0
let item = Stories.Item( let item = Stories.Item(
id: id, id: id,
@ -1311,7 +1326,8 @@ extension Stories.StoredItem {
isExpired: isExpired, isExpired: isExpired,
isPublic: isPublic, isPublic: isPublic,
isCloseFriends: isCloseFriends, isCloseFriends: isCloseFriends,
isForwardingDisabled: isForwardingDisabled isForwardingDisabled: isForwardingDisabled,
isEdited: isEdited
) )
self = .item(item) self = .item(item)
} else { } else {

View File

@ -45,8 +45,9 @@ public final class EngineStoryItem: Equatable {
public let isPending: Bool public let isPending: Bool
public let isCloseFriends: Bool public let isCloseFriends: Bool
public let isForwardingDisabled: Bool public let isForwardingDisabled: Bool
public let isEdited: Bool
public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isForwardingDisabled: Bool) { public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isForwardingDisabled: Bool, isEdited: Bool) {
self.id = id self.id = id
self.timestamp = timestamp self.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp self.expirationTimestamp = expirationTimestamp
@ -61,6 +62,7 @@ public final class EngineStoryItem: Equatable {
self.isPending = isPending self.isPending = isPending
self.isCloseFriends = isCloseFriends self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
} }
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
@ -106,6 +108,9 @@ public final class EngineStoryItem: Equatable {
if lhs.isForwardingDisabled != rhs.isForwardingDisabled { if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false return false
} }
if lhs.isEdited != rhs.isEdited {
return false
}
return true return true
} }
} }
@ -135,7 +140,8 @@ extension EngineStoryItem {
isExpired: self.isExpired, isExpired: self.isExpired,
isPublic: self.isPublic, isPublic: self.isPublic,
isCloseFriends: self.isCloseFriends, isCloseFriends: self.isCloseFriends,
isForwardingDisabled: self.isForwardingDisabled isForwardingDisabled: self.isForwardingDisabled,
isEdited: self.isEdited
) )
} }
} }
@ -494,7 +500,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
items.append(mappedItem) items.append(mappedItem)
} }
@ -601,7 +608,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
storyItems.append(mappedItem) storyItems.append(mappedItem)
} }
@ -735,7 +743,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
finalUpdatedState = updatedState finalUpdatedState = updatedState
} }
@ -774,7 +783,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)) ))
updatedState.items.sort(by: { lhs, rhs in updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp return lhs.timestamp > rhs.timestamp
@ -922,7 +932,8 @@ public final class PeerExpiringStoryListContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
items.append(.item(mappedItem)) items.append(.item(mappedItem))
} }

View File

@ -965,7 +965,8 @@ public extension TelegramEngine {
isExpired: item.isExpired, isExpired: item.isExpired,
isPublic: item.isPublic, isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)) ))
if let entry = CodableEntry(updatedItem) { if let entry = CodableEntry(updatedItem) {
currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp) currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp)

View File

@ -147,7 +147,7 @@ func updatePeerPresences(transaction: Transaction, accountPeerId: PeerId, peerPr
parsedPresences[peerId] = presence parsedPresences[peerId] = presence
default: default:
switch user { switch user {
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let isMin = (flags & (1 << 20)) != 0 let isMin = (flags & (1 << 20)) != 0
if isMin, let _ = transaction.getPeerPresence(peerId: peerId) { if isMin, let _ = transaction.getPeerPresence(peerId: peerId) {
} else { } else {
@ -215,7 +215,7 @@ func updateContacts(transaction: Transaction, apiUsers: [Api.User]) {
for user in apiUsers { for user in apiUsers {
var isContact: Bool? var isContact: Bool?
switch user { switch user {
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _): case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 20)) == 0 { if (flags & (1 << 20)) == 0 {
isContact = (flags & (1 << 11)) != 0 isContact = (flags & (1 << 11)) != 0
} }

View File

@ -1249,33 +1249,23 @@ public struct PresentationResourcesChat {
public static func chatExpiredStoryIndicatorIcon(_ theme: PresentationTheme, type: ChatExpiredStoryIndicatorType) -> UIImage? { public static func chatExpiredStoryIndicatorIcon(_ theme: PresentationTheme, type: ChatExpiredStoryIndicatorType) -> UIImage? {
return theme.image(PresentationResourceParameterKey.chatExpiredStoryIndicatorIcon(type: type), { theme in return theme.image(PresentationResourceParameterKey.chatExpiredStoryIndicatorIcon(type: type), { theme in
return generateImage(CGSize(width: 34.0, height: 34.0), rotatedContext: { size, context in return generateImage(CGSize(width: 16.0, height: 16.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size)) context.clear(CGRect(origin: CGPoint(), size: size))
context.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath)
context.clip()
let color: UIColor
let foregroundColor: UIColor let foregroundColor: UIColor
switch type { switch type {
case .incoming: case .incoming:
color = theme.chat.message.incoming.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.incoming.mediaActiveControlColor foregroundColor = theme.chat.message.incoming.mediaActiveControlColor
case .outgoing: case .outgoing:
color = theme.chat.message.outgoing.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.outgoing.mediaActiveControlColor foregroundColor = theme.chat.message.outgoing.mediaActiveControlColor
case .free: case .free:
color = theme.chat.message.freeform.withWallpaper.reactionActiveMediaPlaceholder foregroundColor = theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText
foregroundColor = theme.chat.message.freeform.withWallpaper.reactionActiveBackground
} }
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: foregroundColor) { if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: foregroundColor) {
UIGraphicsPushContext(context) UIGraphicsPushContext(context)
image.draw(at: CGPoint(x: floor((size.width - image.size.width) * 0.5), y: floor((size.height - image.size.height) * 0.5)), blendMode: .normal, alpha: 1.0) let fittedSize = image.size
image.draw(in: CGRect(origin: CGPoint(x: floor((size.width - fittedSize.width) * 0.5), y: floor((size.height - fittedSize.height) * 0.5)), size: fittedSize), blendMode: .normal, alpha: 1.0)
UIGraphicsPopContext() UIGraphicsPopContext()
} }

View File

@ -900,6 +900,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case .file: case .file:
attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: primaryTextColor) attributedString = NSAttributedString(string: strings.Message_VideoExpired, font: titleFont, textColor: primaryTextColor)
} }
} else if let _ = media as? TelegramMediaStory {
let compactPeerName = message.peers[message.id.peerId].flatMap(EnginePeer.init)?.compactDisplayTitle ?? ""
let resultTitleString: PresentationStrings.FormattedString
if message.flags.contains(.Incoming) {
resultTitleString = PresentationStrings.FormattedString(string: strings.Conversation_StoryExpiredMentionTextIncoming, ranges: [])
} else {
resultTitleString = strings.Conversation_StoryExpiredMentionTextOutgoing(compactPeerName)
}
attributedString = addAttributesToStringWithRanges(resultTitleString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes])
} }
} }

View File

@ -247,13 +247,15 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
if hasPsaInfo { if hasPsaInfo {
infoWidth += 32.0 infoWidth += 32.0
} }
var leftOffset: CGFloat = 0.0 let leftOffset: CGFloat = 0.0
if let storyData, storyData.isExpired {
leftOffset += 34.0 + 6.0
}
infoWidth += leftOffset infoWidth += leftOffset
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) var cutout: TextNodeCutout?
if let storyData, storyData.isExpired {
cutout = TextNodeCutout(topLeft: CGSize(width: 16.0, height: 10.0))
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets()))
return (CGSize(width: textLayout.size.width + credibilityIconWidth + infoWidth, height: textLayout.size.height), { width in return (CGSize(width: textLayout.size.width + credibilityIconWidth + infoWidth, height: textLayout.size.height), { width in
let node: ChatMessageForwardInfoNode let node: ChatMessageForwardInfoNode
@ -290,8 +292,9 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
} }
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(presentationData.theme.theme, type: imageType) expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(presentationData.theme.theme, type: imageType)
if let image = expiredStoryIconView.image { if let _ = expiredStoryIconView.image {
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: image.size) let imageSize = CGSize(width: 18.0, height: 18.0)
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: -1.0, y: -2.0), size: imageSize)
} }
} else if let expiredStoryIconView = node.expiredStoryIconView { } else if let expiredStoryIconView = node.expiredStoryIconView {
expiredStoryIconView.removeFromSuperview() expiredStoryIconView.removeFromSuperview()

View File

@ -14,7 +14,7 @@ public final class EmptyStateIndicatorComponent: Component {
public let animationName: String public let animationName: String
public let title: String public let title: String
public let text: String public let text: String
public let actionTitle: String public let actionTitle: String?
public let action: () -> Void public let action: () -> Void
public init( public init(
@ -23,7 +23,7 @@ public final class EmptyStateIndicatorComponent: Component {
animationName: String, animationName: String,
title: String, title: String,
text: String, text: String,
actionTitle: String, actionTitle: String?,
action: @escaping () -> Void action: @escaping () -> Void
) { ) {
self.context = context self.context = context
@ -64,7 +64,7 @@ public final class EmptyStateIndicatorComponent: Component {
private let animation = ComponentView<Empty>() private let animation = ComponentView<Empty>()
private let title = ComponentView<Empty>() private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>() private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>() private var button: ComponentView<Empty>?
override public init(frame: CGRect) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
@ -108,35 +108,54 @@ public final class EmptyStateIndicatorComponent: Component {
environment: {}, environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0) containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
) )
let buttonSize = self.button.update( var buttonSize: CGSize?
transition: transition, if let actionTitle = component.actionTitle {
component: AnyComponent(ButtonComponent( let button: ComponentView<Empty>
background: ButtonComponent.Background( if let current = self.button {
color: component.theme.list.itemCheckColors.fillColor, button = current
foreground: component.theme.list.itemCheckColors.foregroundColor, } else {
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) button = ComponentView()
), self.button = button
content: AnyComponentWithIdentity(id: 0, component: AnyComponent( }
Text(text: component.actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)
)), buttonSize = button.update(
isEnabled: true, transition: transition,
displaysProgress: false, component: AnyComponent(ButtonComponent(
action: { [weak self] in background: ButtonComponent.Background(
guard let self, let component = self.component else { color: component.theme.list.itemCheckColors.fillColor,
return foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(id: 0, component: AnyComponent(
Text(text: actionTitle, font: Font.semibold(17.0), color: component.theme.list.itemCheckColors.foregroundColor)
)),
isEnabled: true,
displaysProgress: false,
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.action()
} }
component.action() )),
} environment: {},
)), containerSize: CGSize(width: 240.0, height: 50.0)
environment: {}, )
containerSize: CGSize(width: 240.0, height: 50.0) } else {
) if let button = self.button {
self.button = nil
button.view?.removeFromSuperview()
}
}
let animationSpacing: CGFloat = 11.0 let animationSpacing: CGFloat = 11.0
let titleSpacing: CGFloat = 17.0 let titleSpacing: CGFloat = 17.0
let buttonSpacing: CGFloat = 17.0 let buttonSpacing: CGFloat = 17.0
let totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height + buttonSpacing + buttonSize.height var totalHeight: CGFloat = animationSize.height + animationSpacing + titleSize.height + titleSpacing + textSize.height
if let buttonSize {
totalHeight += buttonSpacing + buttonSize.height
}
var contentY = floor((availableSize.height - totalHeight) * 0.5) var contentY = floor((availableSize.height - totalHeight) * 0.5)
@ -161,7 +180,7 @@ public final class EmptyStateIndicatorComponent: Component {
transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentY), size: textSize)) transition.setFrame(view: textView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - textSize.width) * 0.5), y: contentY), size: textSize))
contentY += textSize.height + buttonSpacing contentY += textSize.height + buttonSpacing
} }
if let buttonView = self.button.view { if let buttonSize, let buttonView = self.button?.view {
if buttonView.superview == nil { if buttonView.superview == nil {
self.addSubview(buttonView) self.addSubview(buttonView)
} }

View File

@ -716,7 +716,7 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
index += 1 index += 1
if state.storiesMuted != .alwaysOff { if state.storiesMuted != .alwaysOff {
entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: "Display Author Name")) entries.append(.displayPreviewsHeader(index: index, theme: presentationData.theme, title: "DISPLAY AUTHOR NAME"))
index += 1 index += 1
entries.append(.showSender(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.storiesHideSender == .alwaysOn)) entries.append(.showSender(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.storiesHideSender == .alwaysOn))
index += 1 index += 1

View File

@ -631,7 +631,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
} }
if let selectedMedia = selectedMedia { if let selectedMedia = selectedMedia {
if let result = directMediaImageCache.getImage(peer: item.peer, story: story, media: selectedMedia, width: imageWidthSpec, aspectRatio: 0.56, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler, synchronous: synchronous == .full) { if let result = directMediaImageCache.getImage(peer: item.peer, story: story, media: selectedMedia, width: imageWidthSpec, aspectRatio: 0.72, possibleWidths: SparseItemGridBindingImpl.widthSpecs.1, includeBlurred: hasSpoiler || displayItem.blurLayer != nil, synchronous: synchronous == .full) {
if let image = result.image { if let image = result.image {
layer.setContents(image) layer.setContents(image)
switch synchronous { switch synchronous {
@ -648,6 +648,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
} }
if let image = result.blurredImage { if let image = result.blurredImage {
layer.setSpoilerContents(image) layer.setSpoilerContents(image)
if let blurLayer = displayItem.blurLayer {
blurLayer.contentsGravity = .resizeAspectFill
blurLayer.contents = result.blurredImage?.cgImage
}
} }
if let loadSignal = result.loadSignal { if let loadSignal = result.loadSignal {
layer.disposable?.dispose() layer.disposable?.dispose()
@ -702,6 +707,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
} }
} }
} }
if let displayItem, let blurLayer = displayItem.blurLayer {
blurLayer.contentsGravity = .resizeAspectFill
blurLayer.contents = result.blurredImage?.cgImage
}
}) })
} }
} }
@ -1588,7 +1598,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
} }
var headerText: String? var headerText: String?
if strongSelf.isArchive { if strongSelf.isArchive && !mappedItems.isEmpty {
//TODO:localize //TODO:localize
headerText = "Only you can see archived stories unless you choose to save them to your profile." headerText = "Only you can see archived stories unless you choose to save them to your profile."
} }
@ -1876,7 +1886,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height))) transition.updateFrame(node: self.contextGestureContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)))
if let items = self.items, items.items.isEmpty, items.count == 0, !self.isArchive { if let items = self.items, items.items.isEmpty, items.count == 0 {
let emptyStateView: ComponentView<Empty> let emptyStateView: ComponentView<Empty>
var emptyStateTransition = Transition(transition) var emptyStateTransition = Transition(transition)
if let current = self.emptyStateView { if let current = self.emptyStateView {
@ -1893,9 +1903,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
context: self.context, context: self.context,
theme: presentationData.theme, theme: presentationData.theme,
animationName: "StoryListEmpty", animationName: "StoryListEmpty",
title: "No saved stories", title: self.isArchive ? "No Archived Stories" : "No saved stories",
text: "Open the Archive to select stories you\nwant to be displayed in your profile.", text: self.isArchive ? "Upload a new story to view it here" : "Open the Archive to select stories you\nwant to be displayed in your profile.",
actionTitle: "Open Archive", actionTitle: self.isArchive ? nil : "Open Archive",
action: { [weak self] in action: { [weak self] in
guard let self else { guard let self else {
return return
@ -1948,10 +1958,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
fixedItemHeight = nil fixedItemHeight = nil
} }
let fixedItemAspect: CGFloat? = 9.0 / 16.0 let fixedItemAspect: CGFloat? = 0.72
let gridTopInset = topInset let gridTopInset = topInset
self.itemGrid.pinchEnabled = items.count > 2
self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none) self.itemGrid.update(size: size, insets: UIEdgeInsets(top: gridTopInset, left: sideInset, bottom: bottomInset, right: sideInset), useSideInsets: !isList, scrollIndicatorInsets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: bottomInset, right: sideInset), lockScrollingAtTop: isScrollingLockedAtTop, fixedItemHeight: fixedItemHeight, fixedItemAspect: fixedItemAspect, items: items, theme: self.itemGridBinding.chatPresentationData.theme.theme, synchronous: wasFirstTime ? .full : .none)
} }
} }

View File

@ -20,6 +20,7 @@ public final class AvatarStoryIndicatorComponent: Component {
public let theme: PresentationTheme public let theme: PresentationTheme
public let activeLineWidth: CGFloat public let activeLineWidth: CGFloat
public let inactiveLineWidth: CGFloat public let inactiveLineWidth: CGFloat
public let isGlassBackground: Bool
public let counters: Counters? public let counters: Counters?
public init( public init(
@ -28,6 +29,7 @@ public final class AvatarStoryIndicatorComponent: Component {
theme: PresentationTheme, theme: PresentationTheme,
activeLineWidth: CGFloat, activeLineWidth: CGFloat,
inactiveLineWidth: CGFloat, inactiveLineWidth: CGFloat,
isGlassBackground: Bool = false,
counters: Counters? counters: Counters?
) { ) {
self.hasUnseen = hasUnseen self.hasUnseen = hasUnseen
@ -35,6 +37,7 @@ public final class AvatarStoryIndicatorComponent: Component {
self.theme = theme self.theme = theme
self.activeLineWidth = activeLineWidth self.activeLineWidth = activeLineWidth
self.inactiveLineWidth = inactiveLineWidth self.inactiveLineWidth = inactiveLineWidth
self.isGlassBackground = isGlassBackground
self.counters = counters self.counters = counters
} }
@ -54,6 +57,9 @@ public final class AvatarStoryIndicatorComponent: Component {
if lhs.inactiveLineWidth != rhs.inactiveLineWidth { if lhs.inactiveLineWidth != rhs.inactiveLineWidth {
return false return false
} }
if lhs.isGlassBackground != rhs.isGlassBackground {
return false
}
if lhs.counters != rhs.counters { if lhs.counters != rhs.counters {
return false return false
} }
@ -90,7 +96,7 @@ public final class AvatarStoryIndicatorComponent: Component {
} else { } else {
lineWidth = component.inactiveLineWidth lineWidth = component.inactiveLineWidth
} }
let maxOuterInset = component.activeLineWidth + component.activeLineWidth let maxOuterInset = component.activeLineWidth + lineWidth
diameter = availableSize.width + maxOuterInset * 2.0 diameter = availableSize.width + maxOuterInset * 2.0
let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0 let imageDiameter = availableSize.width + ceilToScreenPixels(maxOuterInset) * 2.0
@ -112,10 +118,14 @@ public final class AvatarStoryIndicatorComponent: Component {
] ]
} }
if component.theme.overallDarkAppearance { if component.isGlassBackground {
inactiveColors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor] inactiveColors = [UIColor(white: 1.0, alpha: 0.2).cgColor, UIColor(white: 1.0, alpha: 0.2).cgColor]
} else { } else {
inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor] if component.theme.overallDarkAppearance {
inactiveColors = [component.theme.rootController.tabBar.textColor.cgColor, component.theme.rootController.tabBar.textColor.cgColor]
} else {
inactiveColors = [UIColor(rgb: 0xD8D8E1).cgColor, UIColor(rgb: 0xD8D8E1).cgColor]
}
} }
var locations: [CGFloat] = [0.0, 1.0] var locations: [CGFloat] = [0.0, 1.0]

View File

@ -95,12 +95,12 @@ final class MediaNavigationStripComponent: Component {
let spacing: CGFloat = 3.0 let spacing: CGFloat = 3.0
let itemHeight: CGFloat = 2.0 let itemHeight: CGFloat = 2.0
let minItemWidth: CGFloat = 10.0 let minItemWidth: CGFloat = 2.0
var validIndices: [Int] = [] var validIndices: [Int] = []
if component.count != 0 { if component.count != 0 {
var idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count) var idealItemWidth: CGFloat = (availableSize.width - CGFloat(component.count - 1) * spacing) / CGFloat(component.count)
idealItemWidth = round(idealItemWidth) idealItemWidth = floor(idealItemWidth)
let itemWidth: CGFloat let itemWidth: CGFloat
if idealItemWidth < minItemWidth { if idealItemWidth < minItemWidth {

View File

@ -10,17 +10,15 @@ final class StoryAuthorInfoComponent: Component {
let context: AccountContext let context: AccountContext
let peer: EnginePeer? let peer: EnginePeer?
let timestamp: Int32 let timestamp: Int32
let isEdited: Bool
init(context: AccountContext, peer: EnginePeer?, timestamp: Int32) { init(context: AccountContext, peer: EnginePeer?, timestamp: Int32, isEdited: Bool) {
self.context = context self.context = context
self.peer = peer self.peer = peer
self.timestamp = timestamp self.timestamp = timestamp
self.isEdited = isEdited
} }
convenience init(context: AccountContext, message: EngineMessage) {
self.init(context: context, peer: message.author, timestamp: message.timestamp)
}
static func ==(lhs: StoryAuthorInfoComponent, rhs: StoryAuthorInfoComponent) -> Bool { static func ==(lhs: StoryAuthorInfoComponent, rhs: StoryAuthorInfoComponent) -> Bool {
if lhs.context !== rhs.context { if lhs.context !== rhs.context {
return false return false
@ -30,6 +28,9 @@ final class StoryAuthorInfoComponent: Component {
} }
if lhs.timestamp != rhs.timestamp { if lhs.timestamp != rhs.timestamp {
return false return false
}
if lhs.isEdited != rhs.isEdited {
return false
} }
return true return true
} }
@ -69,7 +70,12 @@ final class StoryAuthorInfoComponent: Component {
} }
let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970)
let subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp) var subtitle = stringForRelativeActivityTimestamp(strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, relativeTimestamp: component.timestamp, relativeTo: timestamp)
if component.isEdited {
subtitle.append("")
subtitle.append("edited")
}
let titleSize = self.title.update( let titleSize = self.title.update(
transition: .immediate, transition: .immediate,

View File

@ -139,9 +139,11 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: item.isPublic, isPublic: item.isPublic,
isPending: false, isPending: false,
isCloseFriends: item.isCloseFriends, isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
) )
} }
var totalCount = peerStoryItemsView.items.count
if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) { if peerId == context.account.peerId, let stateView = views.views[PostboxViewKey.storiesState(key: .local)] as? StoryStatesView, let localState = stateView.value?.get(Stories.LocalState.self) {
for item in localState.items { for item in localState.items {
mappedItems.append(EngineStoryItem( mappedItems.append(EngineStoryItem(
@ -158,8 +160,10 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: false, isPublic: false,
isPending: true, isPending: true,
isCloseFriends: false, isCloseFriends: false,
isForwardingDisabled: false isForwardingDisabled: false,
isEdited: false
)) ))
totalCount += 1
} }
} }
@ -263,7 +267,7 @@ public final class StoryContentContextImpl: StoryContentContext {
peerId: peer.id, peerId: peer.id,
storyItem: mappedItem storyItem: mappedItem
), ),
totalCount: mappedItems.count, totalCount: totalCount,
previousItemId: previousItemId, previousItemId: previousItemId,
nextItemId: nextItemId, nextItemId: nextItemId,
allItems: allItems allItems: allItems
@ -982,7 +986,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
isPublic: itemValue.isPublic, isPublic: itemValue.isPublic,
isPending: false, isPending: false,
isCloseFriends: itemValue.isCloseFriends, isCloseFriends: itemValue.isCloseFriends,
isForwardingDisabled: itemValue.isForwardingDisabled isForwardingDisabled: itemValue.isForwardingDisabled,
isEdited: itemValue.isEdited
) )
let mainItem = StoryContentItem( let mainItem = StoryContentItem(

View File

@ -516,8 +516,10 @@ private final class StoryContainerScreenComponent: Component {
} }
if subview is ItemSetView { if subview is ItemSetView {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) { if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], itemSetView === subview {
return result if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
} }
} else { } else {
if let result = subview.hitTest(self.convert(self.convert(point, to: subview), to: subview), with: event) { if let result = subview.hitTest(self.convert(self.convert(point, to: subview), to: subview), with: event) {
@ -846,7 +848,7 @@ private final class StoryContainerScreenComponent: Component {
} }
if let stateValue = component.content.stateValue, let slice = stateValue.slice { if let stateValue = component.content.stateValue, let slice = stateValue.slice {
if case .next = direction, slice.nextItemId == nil { if case .next = direction, slice.nextItemId == nil, (slice.item.position == nil || slice.item.position == slice.totalCount - 1) {
if stateValue.nextSlice == nil { if stateValue.nextSlice == nil {
environment.controller()?.dismiss() environment.controller()?.dismiss()
} else { } else {

View File

@ -217,7 +217,7 @@ final class StoryContentCaptionComponent: Component {
let edgeDistanceFraction = edgeDistance / 7.0 let edgeDistanceFraction = edgeDistance / 7.0
transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction) transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction)
let shadowOverflow: CGFloat = 26.0 let shadowOverflow: CGFloat = 36.0
let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow))
transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame) transition.setFrame(layer: self.shadowGradientLayer, frame: shadowFrame)
transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0))) transition.setFrame(layer: self.shadowPlainLayer, frame: CGRect(origin: CGPoint(x: shadowFrame.minX, y: shadowFrame.maxY), size: CGSize(width: shadowFrame.width, height: self.scrollView.contentSize.height + 1000.0)))
@ -364,7 +364,9 @@ final class StoryContentCaptionComponent: Component {
attributedString: attributedText, attributedString: attributedText,
maximumNumberOfLines: 0, maximumNumberOfLines: 0,
truncationType: .end, truncationType: .end,
constrainedSize: textContainerSize constrainedSize: textContainerSize,
textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0
)) ))
let maxHeight: CGFloat = 50.0 let maxHeight: CGFloat = 50.0

View File

@ -139,7 +139,7 @@ final class StoryItemContentComponent: Component {
enableSound: true, enableSound: true,
beginWithAmbientSound: environment.sharedState.useAmbientMode, beginWithAmbientSound: environment.sharedState.useAmbientMode,
useLargeThumbnail: false, useLargeThumbnail: false,
autoFetchFullSizeThumbnail: true, autoFetchFullSizeThumbnail: false,
tempFilePath: nil, tempFilePath: nil,
captureProtected: component.item.isForwardingDisabled, captureProtected: component.item.isForwardingDisabled,
hintDimensions: file.dimensions?.cgSize, hintDimensions: file.dimensions?.cgSize,
@ -431,7 +431,7 @@ final class StoryItemContentComponent: Component {
onlyFullSize: false, onlyFullSize: false,
useLargeThumbnail: false, useLargeThumbnail: false,
synchronousLoad: synchronousLoad, synchronousLoad: synchronousLoad,
autoFetchFullSizeThumbnail: true, autoFetchFullSizeThumbnail: false,
overlayColor: nil, overlayColor: nil,
nilForEmptyResult: false, nilForEmptyResult: false,
useMiniThumbnailIfAvailable: false, useMiniThumbnailIfAvailable: false,

View File

@ -1772,7 +1772,7 @@ public final class StoryItemSetContainerComponent: Component {
guard let self else { guard let self else {
return return
} }
self.navigateToPeer(peer: peer) self.navigateToPeer(peer: peer, chat: false)
} }
)), )),
environment: {}, environment: {},
@ -2082,7 +2082,7 @@ public final class StoryItemSetContainerComponent: Component {
var currentCenterInfoItem: InfoItem? var currentCenterInfoItem: InfoItem?
if focusedItem != nil { if focusedItem != nil {
let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(context: component.context, peer: component.slice.peer, timestamp: component.slice.item.storyItem.timestamp)) let centerInfoComponent = AnyComponent(StoryAuthorInfoComponent(context: component.context, peer: component.slice.peer, timestamp: component.slice.item.storyItem.timestamp, isEdited: component.slice.item.storyItem.isEdited))
if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent { if let centerInfoItem = self.centerInfoItem, centerInfoItem.component == centerInfoComponent {
currentCenterInfoItem = centerInfoItem currentCenterInfoItem = centerInfoItem
} else { } else {
@ -2106,7 +2106,11 @@ public final class StoryItemSetContainerComponent: Component {
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
self.navigateToPeer(peer: component.slice.peer) if component.slice.peer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.peer, chat: false)
}
})), })),
environment: {}, environment: {},
containerSize: CGSize(width: contentFrame.width, height: 44.0) containerSize: CGSize(width: contentFrame.width, height: 44.0)
@ -2136,7 +2140,11 @@ public final class StoryItemSetContainerComponent: Component {
guard let self, let component = self.component else { guard let self, let component = self.component else {
return return
} }
self.navigateToPeer(peer: component.slice.peer) if component.slice.peer.id == component.context.account.peerId {
self.navigateToMyStories()
} else {
self.navigateToPeer(peer: component.slice.peer, chat: false)
}
})), })),
environment: {}, environment: {},
containerSize: CGSize(width: 32.0, height: 32.0) containerSize: CGSize(width: 32.0, height: 32.0)
@ -2433,7 +2441,7 @@ public final class StoryItemSetContainerComponent: Component {
presentationData: presentationData, presentationData: presentationData,
content: .sticker(context: context, file: animation, loop: false, title: nil, text: "Reaction Sent.", undoText: "View in Chat", customAction: { [weak self] in content: .sticker(context: context, file: animation, loop: false, title: nil, text: "Reaction Sent.", undoText: "View in Chat", customAction: { [weak self] in
if let messageId = messageIds.first, let self { if let messageId = messageIds.first, let self {
self.navigateToPeer(peer: peer, messageId: messageId) self.navigateToPeer(peer: peer, chat: true, messageId: messageId)
} }
}), }),
elevatedLayout: false, elevatedLayout: false,
@ -2708,7 +2716,7 @@ public final class StoryItemSetContainerComponent: Component {
}) })
} }
func navigateToPeer(peer: EnginePeer, messageId: EngineMessage.Id? = nil) { func navigateToMyStories() {
guard let component = self.component else { guard let component = self.component else {
return return
} }
@ -2718,8 +2726,34 @@ public final class StoryItemSetContainerComponent: Component {
guard let navigationController = controller.navigationController as? NavigationController else { guard let navigationController = controller.navigationController as? NavigationController else {
return return
} }
if let messageId {
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: .message(id: .id(messageId), highlight: false, timecode: nil), keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in let targetController = component.context.sharedContext.makeMyStoriesController(context: component.context, isArchive: false)
var viewControllers = navigationController.viewControllers
if let index = viewControllers.firstIndex(where: { $0 === controller }) {
viewControllers.insert(targetController, at: index)
} else {
viewControllers.append(targetController)
}
navigationController.setViewControllers(viewControllers, animated: true)
}
func navigateToPeer(peer: EnginePeer, chat: Bool, messageId: EngineMessage.Id? = nil) {
guard let component = self.component else {
return
}
guard let controller = component.controller() as? StoryContainerScreen else {
return
}
guard let navigationController = controller.navigationController as? NavigationController else {
return
}
if messageId != nil || chat {
var subject: ChatControllerSubject?
if let messageId {
subject = .message(id: .id(messageId), highlight: false, timecode: nil)
}
component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in
guard let controller, let navigationController else { guard let controller, let navigationController else {
return return
} }

View File

@ -344,7 +344,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false, animateInAsReplacement: false,
action: { [weak view] action in action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first { if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId) view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
} }
return false return false
} }
@ -386,7 +386,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false, animateInAsReplacement: false,
action: { [weak view] action in action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first { if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId) view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
} }
return false return false
} }
@ -448,7 +448,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false, animateInAsReplacement: false,
action: { [weak view] action in action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first { if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId) view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
} }
return false return false
} }
@ -698,7 +698,7 @@ final class StoryItemSetContainerSendMessage {
let shareController = ShareController( let shareController = ShareController(
context: component.context, context: component.context,
subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id)))), subject: .media(AnyMediaReference.standalone(media: TelegramMediaStory(storyId: StoryId(peerId: peerId, id: focusedItem.storyItem.id), isMention: false))),
preferredAction: preferredAction ?? .default, preferredAction: preferredAction ?? .default,
externalShare: false, externalShare: false,
immediateExternalShare: false, immediateExternalShare: false,
@ -2134,7 +2134,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false, animateInAsReplacement: false,
action: { [weak view] action in action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first { if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId) view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
} }
return false return false
} }

View File

@ -447,8 +447,9 @@ public final class StoryPeerListComponent: Component {
itemView.updateIsPreviewing(isPreviewing: peerId == itemId) itemView.updateIsPreviewing(isPreviewing: peerId == itemId)
if component.unlocked && peerId == itemId { if component.unlocked && peerId == itemId {
if !self.scrollView.bounds.intersects(itemView.frame.insetBy(dx: 20.0, dy: 0.0)) { let itemFrame = itemView.frame.offsetBy(dx: self.scrollView.bounds.minX, dy: 0.0)
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -40.0, dy: 0.0), animated: false) if !self.scrollView.bounds.intersects(itemFrame.insetBy(dx: 20.0, dy: 0.0)) {
self.scrollView.scrollRectToVisible(itemFrame.insetBy(dx: -40.0, dy: 0.0), animated: false)
} }
} }
} }
@ -465,7 +466,7 @@ public final class StoryPeerListComponent: Component {
return nil return nil
} }
if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View { if let visibleItem = self.visibleItems[peerId], let itemView = visibleItem.view.view as? StoryPeerListItemComponent.View {
if !self.scrollView.bounds.intersects(itemView.frame) { if !self.bounds.intersects(itemView.frame) {
return nil return nil
} }

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "ContextStories.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.7635 4.66876C11.842 4.66627 11.9208 4.66502 12 4.66502C12.2275 4.66502 12.4524 4.67536 12.6744 4.69557C13.0401 4.72887 13.3636 4.45936 13.3969 4.09361C13.4302 3.72785 13.1607 3.40435 12.795 3.37105C12.533 3.3472 12.2678 3.33502 12 3.33502C11.9012 3.33502 11.8028 3.33667 11.7048 3.33995C9.50354 3.41363 7.51029 4.30838 6.02163 5.7277C5.90873 5.83534 5.79874 5.94599 5.69177 6.05953C4.28989 7.54763 3.4087 9.53174 3.33938 11.7207C3.33644 11.8134 3.33496 11.9066 3.33496 12C3.33496 12.0982 3.33659 12.196 3.33983 12.2934C3.41063 14.4212 4.2486 16.3547 5.58597 17.8262C5.78988 18.0505 6.00541 18.2641 6.23159 18.4661C7.6731 19.7529 9.54763 20.5649 11.6095 20.6564C11.7389 20.6621 11.8691 20.665 12 20.665C12.2678 20.665 12.533 20.6528 12.795 20.629C13.1607 20.5957 13.4302 20.2722 13.3969 19.9064C13.3636 19.5407 13.0401 19.2712 12.6744 19.3045C12.4524 19.3247 12.2275 19.335 12 19.335C11.8813 19.335 11.7633 19.3322 11.646 19.3266C9.88642 19.243 8.28926 18.5394 7.06807 17.4295C6.87365 17.2528 6.68876 17.0658 6.51426 16.8694C5.41246 15.629 4.72495 14.0124 4.6687 12.2365C4.66621 12.158 4.66496 12.0791 4.66496 12C4.66496 11.9159 4.66638 11.8322 4.66918 11.7488C4.73083 9.91731 5.4639 8.25606 6.63004 7.00336C6.76152 6.86212 6.89851 6.72607 7.04066 6.59556C8.29233 5.44639 9.94435 4.72638 11.7635 4.66876ZM17.5392 5.33648C17.2569 5.10156 16.8376 5.13998 16.6027 5.4223C16.3677 5.70461 16.4062 6.12391 16.6885 6.35882C17.0343 6.64663 17.3534 6.96564 17.6412 7.31151C17.8761 7.59382 18.2954 7.63225 18.5777 7.39733C18.86 7.16241 18.8984 6.74311 18.6635 6.4608C18.3238 6.05261 17.9474 5.67614 17.5392 5.33648ZM20.6289 11.205C20.5956 10.8393 20.2721 10.5698 19.9064 10.6031C19.5406 10.6364 19.2711 10.9599 19.3044 11.3256C19.3246 11.5475 19.335 11.7725 19.335 12C19.335 12.2276 19.3246 12.4525 19.3044 12.6744C19.2711 13.0402 19.5406 13.3637 19.9064 13.397C20.2721 13.4303 20.5956 13.1608 20.6289 12.795C20.6528 12.5331 20.665 12.2679 20.665 12C20.665 11.7322 20.6528 11.467 20.6289 11.205ZM18.6635 17.5392C18.8984 17.2569 18.86 16.8376 18.5777 16.6027C18.2954 16.3678 17.8761 16.4062 17.6412 16.6885C17.3534 17.0344 17.0343 17.3534 16.6885 17.6412C16.4062 17.8761 16.3677 18.2954 16.6027 18.5777C16.8376 18.8601 17.2569 18.8985 17.5392 18.6636C17.9474 18.3239 18.3238 17.9474 18.6635 17.5392Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -1,7 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "timer.pdf", "filename" : "ic_exipred.pdf",
"idiom" : "universal" "idiom" : "universal"
} }
], ],

View File

@ -0,0 +1,120 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.899414 2.555176 cm
1.000000 1.000000 1.000000 scn
0.629534 10.881271 m
0.362801 10.952742 0.088632 10.794451 0.017161 10.527718 c
-0.054310 10.260984 0.103982 9.986816 0.370715 9.915345 c
1.336640 9.656527 l
1.603373 9.585055 1.877542 9.743346 1.949013 10.010079 c
2.020484 10.276813 1.862192 10.550982 1.595459 10.622452 c
0.629534 10.881271 l
h
4.493724 9.846077 m
4.226991 9.917548 3.952822 9.759256 3.881351 9.492523 c
3.809880 9.225790 3.968172 8.951622 4.234905 8.880151 c
5.200830 8.621332 l
5.467563 8.549862 5.741732 8.708153 5.813203 8.974886 c
5.884674 9.241618 5.726382 9.515787 5.459650 9.587257 c
4.493724 9.846077 l
h
2.139100 12.649343 m
2.405833 12.720814 2.680002 12.562522 2.751473 12.295790 c
3.010292 11.329864 l
3.081763 11.063130 2.923471 10.788962 2.656739 10.717491 c
2.390005 10.646021 2.115837 10.804312 2.044366 11.071045 c
1.785547 12.036970 l
1.714076 12.303703 1.872367 12.577872 2.139100 12.649343 c
h
3.622073 11.165578 m
3.426811 10.970316 3.426811 10.653732 3.622073 10.458471 c
3.817335 10.263208 4.133917 10.263208 4.329180 10.458471 c
5.036287 11.165578 l
5.231549 11.360840 5.231549 11.677423 5.036287 11.872684 c
4.841024 12.067946 4.524442 12.067946 4.329180 11.872684 c
3.622073 11.165578 l
h
3.015071 8.419374 m
2.921972 8.755319 3.007470 9.130381 3.271566 9.394478 c
3.466828 9.589739 3.466828 9.906322 3.271566 10.101583 c
3.076304 10.296846 2.759721 10.296846 2.564459 10.101583 c
1.905519 9.442644 1.802501 8.438299 2.255404 7.671359 c
2.210943 7.626899 l
1.820419 7.236375 1.820419 6.603209 2.210943 6.212686 c
2.416687 6.006942 l
1.799674 4.550779 2.084684 2.803301 3.271717 1.616267 c
4.833814 0.054171 7.366474 0.054171 8.928571 1.616267 c
10.490668 3.178365 10.490668 5.711025 8.928571 7.273122 c
7.741580 8.460113 5.994184 8.745144 4.538052 8.128218 c
4.332264 8.334005 l
3.972641 8.693628 3.407257 8.722085 3.015071 8.419374 c
h
4.686750 3.030611 m
3.905701 3.811659 3.905701 5.077989 4.686750 5.859038 c
4.882012 6.054300 4.882012 6.370883 4.686750 6.566144 c
4.491488 6.761407 4.174905 6.761406 3.979643 6.566144 c
2.808070 5.394572 2.808070 3.495077 3.979643 2.323503 c
4.174905 2.128242 4.491488 2.128242 4.686750 2.323503 c
4.882012 2.518766 4.882012 2.835348 4.686750 3.030611 c
h
f*
n
Q
endstream
endobj
3 0 obj
2317
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 16.000000 16.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002407 00000 n
0000002430 00000 n
0000002603 00000 n
0000002677 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2736
%%EOF

View File

@ -1,95 +0,0 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 11.386475 7.000000 cm
0.000000 0.000000 0.000000 scn
3.109776 20.000000 m
1.179763 20.000000 -0.283886 18.259834 0.046805 16.358360 c
0.156231 15.729159 0.419039 15.136623 0.811980 14.633168 c
3.413476 11.300000 l
3.439425 11.257834 l
3.914118 10.486457 3.914118 9.513542 3.439425 8.742166 c
3.413476 8.700000 l
0.811979 5.366831 l
0.419039 4.863377 0.156231 4.270840 0.046805 3.641638 c
-0.283886 1.740166 1.179765 0.000000 3.109779 0.000000 c
8.117176 0.000000 l
10.047190 0.000000 11.510839 1.740166 11.180147 3.641638 c
11.070721 4.270841 10.807913 4.863377 10.414972 5.366832 c
7.813476 8.700000 l
7.787528 8.742166 l
7.312835 9.513542 7.312835 10.486458 7.787528 11.257834 c
7.813476 11.300000 l
10.414973 14.633169 l
10.807914 15.136623 11.070721 15.729160 11.180147 16.358362 c
11.510839 18.259834 10.047188 20.000000 8.117173 20.000000 c
3.109776 20.000000 l
h
7.428961 3.624302 m
6.183511 4.350814 4.643443 4.350814 3.397992 3.624302 c
1.891497 2.745512 l
1.538821 2.539783 1.684751 2.000000 2.093044 2.000000 c
8.733909 2.000000 l
9.142202 2.000000 9.288133 2.539783 8.935456 2.745512 c
7.428961 3.624302 l
h
f*
n
Q
endstream
endobj
3 0 obj
1194
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 34.000000 34.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001284 00000 n
0000001307 00000 n
0000001480 00000 n
0000001554 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1613
%%EOF

View File

@ -318,7 +318,7 @@ private final class ChatHistoryTransactionOpaqueState {
} }
} }
private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, alwaysDisplayTranscribeButton: ChatMessageItemAssociatedData.DisplayTranscribeButton, accountPeer: EnginePeer?, topicAuthorId: EnginePeer.Id?, hasBots: Bool, translateToLanguage: String?) -> ChatMessageItemAssociatedData { private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHistoryView, automaticDownloadNetworkType: MediaAutoDownloadNetworkType, animatedEmojiStickers: [String: [StickerPackItem]], additionalAnimatedEmojiStickers: [String: [Int: StickerPackItem]], subject: ChatControllerSubject?, currentlyPlayingMessageId: MessageIndex?, isCopyProtectionEnabled: Bool, availableReactions: AvailableReactions?, defaultReaction: MessageReaction.Reaction?, isPremium: Bool, alwaysDisplayTranscribeButton: ChatMessageItemAssociatedData.DisplayTranscribeButton, accountPeer: EnginePeer?, topicAuthorId: EnginePeer.Id?, hasBots: Bool, translateToLanguage: String?, maxReadStoryId: Int32?) -> ChatMessageItemAssociatedData {
var automaticDownloadPeerId: EnginePeer.Id? var automaticDownloadPeerId: EnginePeer.Id?
var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel var automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
var contactsPeerIds: Set<PeerId> = Set() var contactsPeerIds: Set<PeerId> = Set()
@ -372,7 +372,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist
automaticDownloadPeerId = message.messageId.peerId automaticDownloadPeerId = message.messageId.peerId
} }
return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage) return ChatMessageItemAssociatedData(automaticDownloadPeerType: automaticMediaDownloadPeerType, automaticDownloadPeerId: automaticDownloadPeerId, automaticDownloadNetworkType: automaticDownloadNetworkType, isRecentActions: false, subject: subject, contactsPeerIds: contactsPeerIds, channelDiscussionGroup: channelDiscussionGroup, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, currentlyPlayingMessageId: currentlyPlayingMessageId, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, accountPeer: accountPeer, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, topicAuthorId: topicAuthorId, hasBots: hasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId)
} }
private extension ChatHistoryLocationInput { private extension ChatHistoryLocationInput {
@ -1086,6 +1086,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
self.chatHasBotsPromise.get() self.chatHasBotsPromise.get()
) )
let maxReadStoryId: Signal<Int32?, NoError>
if let peerId = chatLocation.peerId, peerId.namespace == Namespaces.Peer.CloudUser {
maxReadStoryId = context.account.postbox.combinedView(keys: [PostboxViewKey.storiesState(key: .peer(peerId))])
|> map { views -> Int32? in
guard let view = views.views[PostboxViewKey.storiesState(key: .peer(peerId))] as? StoryStatesView else {
return nil
}
if let state = view.value?.get(Stories.PeerState.self) {
return state.maxReadId
} else {
return nil
}
}
|> distinctUntilChanged
} else {
maxReadStoryId = .single(nil)
}
let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue, let historyViewTransitionDisposable = combineLatest(queue: messageViewQueue,
historyViewUpdate, historyViewUpdate,
self.chatPresentationDataPromise.get(), self.chatPresentationDataPromise.get(),
@ -1103,8 +1121,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
promises, promises,
topicAuthorId, topicAuthorId,
self.allAdMessagesPromise.get(), self.allAdMessagesPromise.get(),
translationState translationState,
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, allAdMessages, translationState in maxReadStoryId
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, allAdMessages, translationState, maxReadStoryId in
let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots) = promises let (historyAppearsCleared, pendingUnpinnedAllMessages, pendingRemovedMessages, currentlyPlayingMessageIdAndType, scrollToMessageId, chatHasBots) = promises
func applyHole() { func applyHole() {
@ -1260,7 +1279,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
translateToLanguage = languageCode translateToLanguage = languageCode
} }
let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage) let associatedData = extractAssociatedData(chatLocation: chatLocation, view: view, automaticDownloadNetworkType: networkType, animatedEmojiStickers: animatedEmojiStickers, additionalAnimatedEmojiStickers: additionalAnimatedEmojiStickers, subject: subject, currentlyPlayingMessageId: currentlyPlayingMessageIdAndType?.0, isCopyProtectionEnabled: isCopyProtectionEnabled, availableReactions: availableReactions, defaultReaction: defaultReaction, isPremium: isPremium, alwaysDisplayTranscribeButton: alwaysDisplayTranscribeButton, accountPeer: accountPeer, topicAuthorId: topicAuthorId, hasBots: chatHasBots, translateToLanguage: translateToLanguage, maxReadStoryId: maxReadStoryId)
let filteredEntries = chatHistoryEntriesForView( let filteredEntries = chatHistoryEntriesForView(
location: chatLocation, location: chatLocation,

View File

@ -39,6 +39,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
private var videoStartTimestamp: Double? private var videoStartTimestamp: Double?
private let fetchDisposable = MetaDisposable() private let fetchDisposable = MetaDisposable()
private var leadingIconView: UIImageView?
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])? private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
private var absoluteRect: (CGRect, CGSize)? private var absoluteRect: (CGRect, CGSize)?
@ -167,6 +169,7 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview) let attributedString = attributedServiceMessageString(theme: item.presentationData.theme, strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, dateTimeFormat: item.presentationData.dateTimeFormat, message: item.message, accountPeerId: item.context.account.peerId, forForumOverview: forForumOverview)
var image: TelegramMediaImage? var image: TelegramMediaImage?
var story: TelegramMediaStory?
for media in item.message.media { for media in item.message.media {
if let action = media as? TelegramMediaAction { if let action = media as? TelegramMediaAction {
switch action.action { switch action.action {
@ -175,12 +178,21 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
default: default:
break break
} }
} else if let media = media as? TelegramMediaStory {
story = media
} }
} }
let imageSize = CGSize(width: 212.0, height: 212.0) let imageSize = CGSize(width: 212.0, height: 212.0)
let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets())) var updatedAttributedString = attributedString
if story != nil, let attributedString {
let mutableString = NSMutableAttributedString(attributedString: attributedString)
mutableString.insert(NSAttributedString(string: " ", font: Font.regular(13.0), textColor: .clear), at: 0)
updatedAttributedString = mutableString
}
let (labelLayout, apply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: updatedAttributedString, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
var labelRects = labelLayout.linesRects() var labelRects = labelLayout.linesRects()
if labelRects.count > 1 { if labelRects.count > 1 {
@ -306,6 +318,28 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
)) ))
let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size) let labelFrame = CGRect(origin: CGPoint(x: 8.0, y: image != nil ? 2 : floorToScreenPixels((backgroundSize.height - labelLayout.size.height) / 2.0) - 1.0), size: labelLayout.size)
if story != nil {
let leadingIconView: UIImageView
if let current = strongSelf.leadingIconView {
leadingIconView = current
} else {
leadingIconView = UIImageView()
strongSelf.leadingIconView = leadingIconView
strongSelf.view.addSubview(leadingIconView)
}
leadingIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(item.presentationData.theme.theme, type: .free)
if let lineRect = labelLayout.linesRects().first, let iconImage = leadingIconView.image {
let iconSize = iconImage.size
leadingIconView.frame = CGRect(origin: CGPoint(x: lineRect.minX + labelFrame.minX - 1.0, y: labelFrame.minY), size: iconSize)
}
} else if let leadingIconView = strongSelf.leadingIconView {
strongSelf.leadingIconView = nil
leadingIconView.removeFromSuperview()
}
strongSelf.labelNode.textNode.frame = labelFrame strongSelf.labelNode.textNode.frame = labelFrame
strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper) strongSelf.backgroundColorNode.backgroundColor = selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper)

View File

@ -129,12 +129,17 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
} }
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
} else if let story = media as? TelegramMediaStory { } else if let story = media as? TelegramMediaStory {
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty { if story.isMention {
messageWithCaptionToAdd = (message, itemAttributes) if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty {
} result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty { } else {
result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
} else { } else {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty {
} else {
result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default)))
}
} }
} else if let file = media as? TelegramMediaFile { } else if let file = media as? TelegramMediaFile {
let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil) let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil)
@ -229,7 +234,14 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
inner: for media in message.media { inner: for media in message.media {
if let webpage = media as? TelegramMediaWebpage { if let webpage = media as? TelegramMediaWebpage {
if case .Loaded = webpage.content { if case let .Loaded(content) = webpage.content {
if let story = content.story {
if let storyItem = message.associatedStories[story.storyId], !storyItem.data.isEmpty {
} else {
break inner
}
}
result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default))) result.append((message, ChatMessageWebpageBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
needReactions = false needReactions = false
} }

View File

@ -81,6 +81,9 @@ public enum ChatMessageItemContent: Sequence {
} }
private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge { private func mediaMergeableStyle(_ media: Media) -> ChatMessageMerge {
if let story = media as? TelegramMediaStory, story.isMention {
return .none
}
if let file = media as? TelegramMediaFile { if let file = media as? TelegramMediaFile {
for attribute in file.attributes { for attribute in file.attributes {
switch attribute { switch attribute {

View File

@ -137,13 +137,14 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
titleString = arguments.strings.User_DeletedAccount titleString = arguments.strings.User_DeletedAccount
} }
//TODO:localize //TODO:localize
isMedia = true
isText = false isText = false
if let storyItem = arguments.parentMessage.associatedStories[story], storyItem.data.isEmpty { if let storyItem = arguments.parentMessage.associatedStories[story], storyItem.data.isEmpty {
isExpiredStory = true isExpiredStory = true
textString = NSAttributedString(string: "Expired story") textString = NSAttributedString(string: "Expired story")
isMedia = false
} else { } else {
textString = NSAttributedString(string: "Story") textString = NSAttributedString(string: "Story")
isMedia = true
} }
} else { } else {
titleString = " " titleString = " "
@ -187,7 +188,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
case let .bubble(incoming): case let .bubble(incoming):
titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor titleColor = incoming ? (authorNameColor ?? arguments.presentationData.theme.theme.chat.message.incoming.accentTextColor) : arguments.presentationData.theme.theme.chat.message.outgoing.accentTextColor
lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme) lineImage = incoming ? (authorNameColor.flatMap({ PresentationResourcesChat.chatBubbleVerticalLineImage(color: $0) }) ?? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(arguments.presentationData.theme.theme)) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(arguments.presentationData.theme.theme)
if isMedia { if isMedia || isExpiredStory {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
} else { } else {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.primaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.primaryTextColor
@ -283,8 +284,6 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
imageDimensions = representation.dimensions.cgSize imageDimensions = representation.dimensions.cgSize
} }
} }
} else if storyItem.data.isEmpty {
imageDimensions = CGSize(width: 34.0, height: 34.0)
} }
} }
@ -295,19 +294,18 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset) let maximumTextWidth = max(0.0, arguments.constrainedSize.width - imageTextInset)
let contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height) var contrainedTextSize = CGSize(width: maximumTextWidth, height: arguments.constrainedSize.height)
let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0) let textInsets = UIEdgeInsets(top: 3.0, left: 0.0, bottom: 3.0, right: 0.0)
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: titleString, font: titleFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
if isExpiredStory {
contrainedTextSize.width -= 24.0
}
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets)) let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: messageText, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: contrainedTextSize, alignment: .natural, cutout: nil, insets: textInsets))
let imageSide: CGFloat let imageSide: CGFloat
if isExpiredStory { imageSide = titleLayout.size.height + textLayout.size.height - 16.0
imageSide = 38.0
} else {
imageSide = titleLayout.size.height + textLayout.size.height - 16.0
}
var applyImage: (() -> TransformImageNode)? var applyImage: (() -> TransformImageNode)?
if let imageDimensions = imageDimensions { if let imageDimensions = imageDimensions {
@ -357,9 +355,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
} }
} }
} }
let _ = isExpiredStory
let size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing) var size = CGSize(width: max(titleLayout.size.width - textInsets.left - textInsets.right, textLayout.size.width - textInsets.left - textInsets.right) + leftInset, height: titleLayout.size.height + textLayout.size.height - 2 * (textInsets.top + textInsets.bottom) + 2 * spacing)
if isExpiredStory {
size.width += 14.0
}
return (size, { attemptSynchronous in return (size, { attemptSynchronous in
let node: ChatMessageReplyInfoNode let node: ChatMessageReplyInfoNode
@ -414,6 +414,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
node.imageNode?.captureProtected = message.isCopyProtected() node.imageNode?.captureProtected = message.isCopyProtected()
} }
titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size)
textNode.textNode.frame = textFrame.offsetBy(dx: isExpiredStory ? 16.0 : 0.0, dy: 0.0)
if isExpiredStory { if isExpiredStory {
let expiredStoryIconView: UIImageView let expiredStoryIconView: UIImageView
if let current = node.expiredStoryIconView { if let current = node.expiredStoryIconView {
@ -434,17 +439,13 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(arguments.presentationData.theme.theme, type: imageType) expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(arguments.presentationData.theme.theme, type: imageType)
if let image = expiredStoryIconView.image { if let image = expiredStoryIconView.image {
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: 8.0, y: 3.0), size: image.size) let imageSize = CGSize(width: floor(image.size.width * 1.22), height: floor(image.size.height * 1.22))
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: textFrame.minX - 2.0, y: textFrame.minY + 2.0), size: imageSize)
} }
} else if let expiredStoryIconView = node.expiredStoryIconView { } else if let expiredStoryIconView = node.expiredStoryIconView {
expiredStoryIconView.removeFromSuperview() expiredStoryIconView.removeFromSuperview()
} }
titleNode.frame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: spacing - textInsets.top + 1.0), size: titleLayout.size)
let textFrame = CGRect(origin: CGPoint(x: leftInset - textInsets.left - 2.0, y: titleNode.frame.maxY - textInsets.bottom + spacing - textInsets.top - 2.0), size: textLayout.size)
textNode.textNode.frame = textFrame
if !textLayout.spoilers.isEmpty { if !textLayout.spoilers.isEmpty {
let dustNode: InvisibleInkDustNode let dustNode: InvisibleInkDustNode
if let current = node.dustNode { if let current = node.dustNode {

View File

@ -0,0 +1,368 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import TextFormat
import LocalizedPeerData
import TelegramStringFormatting
import WallpaperBackgroundNode
import ReactionSelectionNode
import PhotoResources
import UniversalMediaPlayer
import TelegramUniversalVideoContent
import GalleryUI
import Markdown
import ComponentFlow
import AvatarStoryIndicatorComponent
import AvatarNode
class ChatMessageStoryMentionContentNode: ChatMessageBubbleContentNode {
private var mediaBackgroundContent: WallpaperBubbleBackgroundNode?
private let mediaBackgroundNode: NavigationBackgroundNode
private let subtitleNode: TextNode
private let imageNode: TransformImageNode
private let storyIndicator = ComponentView<Empty>()
private let buttonNode: HighlightTrackingButtonNode
private let buttonTitleNode: TextNode
private var absoluteRect: (CGRect, CGSize)?
private let fetchDisposable = MetaDisposable()
required init() {
self.mediaBackgroundNode = NavigationBackgroundNode(color: .clear)
self.mediaBackgroundNode.clipsToBounds = true
self.mediaBackgroundNode.cornerRadius = 24.0
self.subtitleNode = TextNode()
self.subtitleNode.isUserInteractionEnabled = false
self.subtitleNode.displaysAsynchronously = false
self.imageNode = TransformImageNode()
self.imageNode.contentAnimations = [.subsequentUpdates]
self.buttonNode = HighlightTrackingButtonNode()
self.buttonNode.clipsToBounds = true
self.buttonNode.cornerRadius = 17.0
self.buttonTitleNode = TextNode()
self.buttonTitleNode.isUserInteractionEnabled = false
self.buttonTitleNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.mediaBackgroundNode)
self.addSubnode(self.subtitleNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.buttonTitleNode)
self.buttonNode.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.buttonNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonNode.alpha = 0.4
strongSelf.buttonTitleNode.layer.removeAnimation(forKey: "opacity")
strongSelf.buttonTitleNode.alpha = 0.4
} else {
strongSelf.buttonNode.alpha = 1.0
strongSelf.buttonNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
strongSelf.buttonTitleNode.alpha = 1.0
strongSelf.buttonTitleNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
}
}
}
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.fetchDisposable.dispose()
}
override func transitionNode(messageId: MessageId, media: Media, adjustRect: Bool) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? {
if self.item?.message.id == messageId {
return (self.imageNode, self.imageNode.bounds, { [weak self] in
guard let strongSelf = self else {
return (nil, nil)
}
let resultView = strongSelf.imageNode.view.snapshotContentTree(unhide: true)
return (resultView, nil)
})
} else {
return nil
}
}
override func updateHiddenMedia(_ media: [Media]?) -> Bool {
var mediaHidden = false
var currentMedia: Media?
if let item = item {
mediaLoop: for media in item.message.media {
if let media = media as? TelegramMediaStory {
currentMedia = media
}
}
}
if let currentMedia = currentMedia, let media = media {
for item in media {
if item.isSemanticallyEqual(to: currentMedia) {
mediaHidden = true
break
}
}
}
self.imageNode.isHidden = mediaHidden
return mediaHidden
}
@objc private func buttonPressed() {
guard let item = self.item else {
return
}
let _ = item.controllerInteraction.openMessage(item.message, .default)
}
override func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, unboundSize: CGSize?, maxWidth: CGFloat, layout: (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
let makeImageLayout = self.imageNode.asyncLayout()
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
let makeButtonTitleLayout = TextNode.asyncLayout(self.buttonTitleNode)
let currentItem = self.item
return { item, layoutConstants, _, _, _, _ in
let contentProperties = ChatMessageBubbleContentProperties(hidesSimpleAuthorHeader: true, headerSpacing: 0.0, hidesBackground: .always, forceFullCorners: false, forceAlignment: .center)
return (contentProperties, nil, CGFloat.greatestFiniteMagnitude, { constrainedSize, position in
let width: CGFloat = 180.0
let imageSize = CGSize(width: 100.0, height: 100.0)
let primaryTextColor = serviceMessageColorComponents(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper).primaryText
var story: Stories.Item?
let storyMedia: TelegramMediaStory? = item.message.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory
var selectedMedia: Media?
if let storyMedia, let storyItem = item.message.associatedStories[storyMedia.storyId], !storyItem.data.isEmpty, case let .item(storyValue) = storyItem.get(Stories.StoredItem.self) {
selectedMedia = storyValue.media
story = storyValue
}
var mediaUpdated = false
if let selectedMedia, let currentItem, let storyMedia = currentItem.message.media.first(where: { $0 is TelegramMediaStory }) as? TelegramMediaStory, let storyItem = currentItem.message.associatedStories[storyMedia.storyId], !storyItem.data.isEmpty, case let .item(storyValue) = storyItem.get(Stories.StoredItem.self) {
if let currentMedia = storyValue.media {
mediaUpdated = !selectedMedia.isSemanticallyEqual(to: currentMedia)
} else {
mediaUpdated = true
}
} else {
mediaUpdated = true
}
let fromYou = item.message.author?.id == item.context.account.peerId
let peerName = item.message.peers[item.message.id.peerId].flatMap { EnginePeer($0).compactDisplayTitle } ?? ""
let textWithRanges: PresentationStrings.FormattedString
if fromYou {
textWithRanges = item.presentationData.strings.Conversation_StoryMentionTextOutgoing(peerName)
} else {
textWithRanges = item.presentationData.strings.Conversation_StoryMentionTextIncoming(peerName)
}
let text = NSMutableAttributedString()
text.append(NSAttributedString(string: textWithRanges.string, font: Font.regular(13.0), textColor: primaryTextColor))
for range in textWithRanges.ranges {
if range.index == 0 {
text.addAttribute(.font, value: Font.semibold(13.0), range: range.range)
}
}
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: text, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
//TODO:localize
let (buttonTitleLayout, buttonTitleApply) = makeButtonTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: "View Story", font: Font.semibold(15.0), textColor: primaryTextColor, paragraphAlignment: .center), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: width - 32.0, height: CGFloat.greatestFiniteMagnitude), alignment: .center, cutout: nil, insets: UIEdgeInsets()))
let backgroundSize = CGSize(width: width, height: subtitleLayout.size.height + 186.0)
return (backgroundSize.width, { boundingWidth in
return (backgroundSize, { [weak self] animation, synchronousLoads, _ in
if let strongSelf = self {
strongSelf.item = item
let imageFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - imageSize.width) / 2.0), y: 15.0), size: imageSize).insetBy(dx: 6.0, dy: 6.0)
if let story, let selectedMedia {
if mediaUpdated {
if story.isForwardingDisabled {
let maxImageSize = CGSize(width: 180.0, height: 180.0)
let boundingImageSize = maxImageSize
var updateImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
if let author = item.message.author {
updateImageSignal = peerAvatarCompleteImage(account: item.context.account, peer: EnginePeer(author), size: imageSize)
|> map { image in
return { arguments in
let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: true)
context?.withContext { c in
UIGraphicsPushContext(c)
c.addEllipse(in: CGRect(origin: CGPoint(), size: arguments.drawingSize))
c.clip()
if let image {
image.draw(in: arguments.imageRect)
}
UIGraphicsPopContext()
}
return context
}
}
}
if let updateImageSignal {
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
}
let arguments = TransformImageArguments(corners: ImageCorners(radius: imageFrame.height / 2.0), imageSize: boundingImageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets())
let apply = makeImageLayout(arguments)
apply()
strongSelf.imageNode.frame = imageFrame
} else if let photo = selectedMedia as? TelegramMediaImage {
let maxImageSize = photo.representations.last?.dimensions.cgSize ?? imageFrame.size
let boundingImageSize = maxImageSize.aspectFilled(imageFrame.size)
strongSelf.fetchDisposable.set(chatMessagePhotoInteractiveFetched(context: item.context, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), displayAtSize: nil, storeToDownloadsPeerId: nil).start())
let updateImageSignal = chatMessagePhoto(postbox: item.context.account.postbox, userLocation: .peer(item.message.id.peerId), photoReference: .message(message: MessageReference(item.message), media: photo), synchronousLoad: synchronousLoads)
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
let arguments = TransformImageArguments(corners: ImageCorners(radius: imageFrame.height / 2.0), imageSize: boundingImageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets())
let apply = makeImageLayout(arguments)
apply()
strongSelf.imageNode.frame = imageFrame
} else if let file = selectedMedia as? TelegramMediaFile {
let maxImageSize = file.dimensions?.cgSize ?? imageFrame.size
let boundingImageSize = maxImageSize.aspectFilled(imageFrame.size)
let updateImageSignal = chatMessageVideoThumbnail(account: item.context.account, userLocation: .peer(item.message.id.peerId), fileReference: .message(message: MessageReference(item.message), media: file), blurred: false, synchronousLoads: synchronousLoads)
strongSelf.imageNode.setSignal(updateImageSignal, attemptSynchronously: synchronousLoads)
let arguments = TransformImageArguments(corners: ImageCorners(radius: imageFrame.width / 2.0), imageSize: boundingImageSize, boundingSize: imageFrame.size, intrinsicInsets: UIEdgeInsets())
let apply = makeImageLayout(arguments)
apply()
strongSelf.imageNode.frame = imageFrame
}
}
}
if let storyMedia {
var hasUnseen = false
if !fromYou {
hasUnseen = item.associatedData.maxReadStoryId.flatMap({ $0 < storyMedia.storyId.id }) ?? false
}
let indicatorFrame = imageFrame
let _ = strongSelf.storyIndicator.update(
transition: .immediate,
component: AnyComponent(AvatarStoryIndicatorComponent(
hasUnseen: hasUnseen,
hasUnseenCloseFriendsItems: hasUnseen && (story?.isCloseFriends ?? false),
theme: item.presentationData.theme.theme,
activeLineWidth: 3.0,
inactiveLineWidth: 1.0 + UIScreenPixel,
isGlassBackground: true,
counters: nil
)),
environment: {},
containerSize: indicatorFrame.size
)
if let storyIndicatorView = strongSelf.storyIndicator.view {
if storyIndicatorView.superview == nil {
strongSelf.view.addSubview(storyIndicatorView)
}
storyIndicatorView.frame = indicatorFrame
}
}
let mediaBackgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((backgroundSize.width - width) / 2.0), y: 0.0), size: backgroundSize)
strongSelf.mediaBackgroundNode.frame = mediaBackgroundFrame
strongSelf.mediaBackgroundNode.updateColor(color: selectDateFillStaticColor(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), enableBlur: item.controllerInteraction.enableFullTranslucency && dateFillNeedsBlur(theme: item.presentationData.theme.theme, wallpaper: item.presentationData.theme.wallpaper), transition: .immediate)
strongSelf.mediaBackgroundNode.update(size: mediaBackgroundFrame.size, transition: .immediate)
strongSelf.buttonNode.backgroundColor = item.presentationData.theme.theme.overallDarkAppearance ? UIColor(rgb: 0xffffff, alpha: 0.12) : UIColor(rgb: 0x000000, alpha: 0.12)
let _ = subtitleApply()
let _ = buttonTitleApply()
let subtitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - subtitleLayout.size.width) / 2.0) , y: mediaBackgroundFrame.minY + 128.0), size: subtitleLayout.size)
strongSelf.subtitleNode.frame = subtitleFrame
let buttonTitleFrame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonTitleLayout.size.width) / 2.0), y: subtitleFrame.maxY + 19.0), size: buttonTitleLayout.size)
strongSelf.buttonTitleNode.frame = buttonTitleFrame
let buttonSize = CGSize(width: buttonTitleLayout.size.width + 38.0, height: 34.0)
strongSelf.buttonNode.frame = CGRect(origin: CGPoint(x: mediaBackgroundFrame.minX + floorToScreenPixels((mediaBackgroundFrame.width - buttonSize.width) / 2.0), y: subtitleFrame.maxY + 11.0), size: buttonSize)
if item.controllerInteraction.presentationContext.backgroundNode?.hasExtraBubbleBackground() == true {
if strongSelf.mediaBackgroundContent == nil, let backgroundContent = item.controllerInteraction.presentationContext.backgroundNode?.makeBubbleBackground(for: .free) {
strongSelf.mediaBackgroundNode.isHidden = true
backgroundContent.clipsToBounds = true
backgroundContent.allowsGroupOpacity = true
backgroundContent.cornerRadius = 24.0
strongSelf.mediaBackgroundContent = backgroundContent
strongSelf.insertSubnode(backgroundContent, at: 0)
}
strongSelf.mediaBackgroundContent?.frame = mediaBackgroundFrame
} else {
strongSelf.mediaBackgroundNode.isHidden = false
strongSelf.mediaBackgroundContent?.removeFromSupernode()
strongSelf.mediaBackgroundContent = nil
}
if let (rect, size) = strongSelf.absoluteRect {
strongSelf.updateAbsoluteRect(rect, within: size)
}
}
})
})
})
}
}
override func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
self.absoluteRect = (rect, containerSize)
if let mediaBackgroundContent = self.mediaBackgroundContent {
var backgroundFrame = mediaBackgroundContent.frame
backgroundFrame.origin.x += rect.minX
backgroundFrame.origin.y += rect.minY
mediaBackgroundContent.update(rect: backgroundFrame, within: containerSize, transition: .immediate)
}
}
override func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
if self.mediaBackgroundNode.frame.contains(point) {
return .openMessage
} else {
return .none
}
}
}

View File

@ -47,10 +47,14 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
selectedTransitionNode = params.transitionNode(params.message.id, story, true) selectedTransitionNode = params.transitionNode(params.message.id, story, true)
if let selectedTransitionNode { if let selectedTransitionNode {
var cornerRadius: CGFloat = 0.0
if let imageNode = selectedTransitionNode.0 as? TransformImageNode, let currentArguments = imageNode.currentArguments {
cornerRadius = currentArguments.corners.topLeft.radius
}
transitionIn = StoryContainerScreen.TransitionIn( transitionIn = StoryContainerScreen.TransitionIn(
sourceView: selectedTransitionNode.0.view, sourceView: selectedTransitionNode.0.view,
sourceRect: selectedTransitionNode.1, sourceRect: selectedTransitionNode.1,
sourceCornerRadius: 0.0, sourceCornerRadius: cornerRadius,
sourceIsAvatar: false sourceIsAvatar: false
) )
} }
@ -67,6 +71,11 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
selectedTransitionNode = params.transitionNode(params.message.id, story, true) selectedTransitionNode = params.transitionNode(params.message.id, story, true)
if let selectedTransitionNode { if let selectedTransitionNode {
var cornerRadius: CGFloat = 0.0
if let imageNode = selectedTransitionNode.0 as? TransformImageNode, let currentArguments = imageNode.currentArguments {
cornerRadius = currentArguments.corners.topLeft.radius
}
transitionOut = StoryContainerScreen.TransitionOut( transitionOut = StoryContainerScreen.TransitionOut(
destinationView: selectedTransitionNode.0.view, destinationView: selectedTransitionNode.0.view,
transitionView: StoryContainerScreen.TransitionView( transitionView: StoryContainerScreen.TransitionView(
@ -83,20 +92,23 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
return return
} }
if state.progress == 0.0 { if state.progress == 0.0 {
view.frame = CGRect(origin: CGPoint(), size: state.sourceSize) view.frame = CGRect(origin: CGPoint(), size: state.destinationSize)
} }
let toScale = state.sourceSize.width / state.destinationSize.width let toScaleX = state.sourceSize.width / state.destinationSize.width
let fromScale: CGFloat = 1.0 let toScaleY = state.sourceSize.height / state.destinationSize.height
let scale = toScale.interpolate(to: fromScale, amount: state.progress) let fromScaleX: CGFloat = 1.0
transition.setTransform(view: view, transform: CATransform3DMakeScale(scale, scale, 1.0)) let fromScaleY: CGFloat = 1.0
let scaleX = toScaleX.interpolate(to: fromScaleX, amount: state.progress)
let scaleY = toScaleY.interpolate(to: fromScaleY, amount: state.progress)
transition.setTransform(view: view, transform: CATransform3DMakeScale(scaleX, scaleY, 1.0))
}, },
insertCloneTransitionView: { view in insertCloneTransitionView: { view in
params.addToTransitionSurface(view) params.addToTransitionSurface(view)
} }
), ),
destinationRect: selectedTransitionNode.1, destinationRect: selectedTransitionNode.1,
destinationCornerRadius: 0.0, destinationCornerRadius: cornerRadius,
destinationIsAvatar: false, destinationIsAvatar: false,
completed: { completed: {
params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource) params.context.sharedContext.mediaManager.galleryHiddenMediaManager.removeSource(hiddenMediaSource)

View File

@ -722,7 +722,7 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen
|> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories -> PeerInfoScreenData in |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories -> PeerInfoScreenData in
var availablePanes = availablePanes var availablePanes = availablePanes
if hasStories, peerView.peers[peerView.peerId] is TelegramUser { if hasStories, peerView.peers[peerView.peerId] is TelegramUser, peerView.peerId != context.account.peerId {
availablePanes?.insert(.stories, at: 0) availablePanes?.insert(.stories, at: 0)
} }

View File

@ -38,6 +38,7 @@ import ChatTextLinkEditUI
import AttachmentTextInputPanelNode import AttachmentTextInputPanelNode
import ChatEntityKeyboardInputNode import ChatEntityKeyboardInputNode
import HashtagSearchUI import HashtagSearchUI
import PeerInfoStoryGridScreen
private final class AccountUserInterfaceInUseContext { private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>() let subscribers = Bag<(Bool) -> Void>()
@ -1723,6 +1724,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
return HashtagSearchController(context: context, peer: peer, query: query) return HashtagSearchController(context: context, peer: peer, query: query)
} }
public func makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController {
return PeerInfoStoryGridScreen(context: context, peerId: context.account.peerId, scope: isArchive ? .archive : .saved)
}
public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController { public func makePremiumIntroController(context: AccountContext, source: PremiumIntroSource) -> ViewController {
let mappedSource: PremiumSource let mappedSource: PremiumSource
switch source { switch source {

View File

@ -26,6 +26,7 @@ import AvatarNode
import LocalMediaResources import LocalMediaResources
import ImageCompression import ImageCompression
import TextFormat import TextFormat
import MediaEditor
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
private var presentationData: PresentationData private var presentationData: PresentationData
@ -405,7 +406,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
} }
dismissCameraImpl?() dismissCameraImpl?()
} } as (Int64, MediaEditorScreen.Result?, NSAttributedString, MediaEditorResultPrivacy, [TelegramMediaFile], @escaping (@escaping () -> Void) -> Void) -> Void
) )
controller.cancelled = { showDraftTooltip in controller.cancelled = { showDraftTooltip in
if showDraftTooltip { if showDraftTooltip {