This commit is contained in:
Ali 2023-06-30 19:48:37 +02:00
parent 1abb7efc75
commit 84dcde0051
55 changed files with 1138 additions and 362 deletions

View File

@ -9373,3 +9373,8 @@ Sorry for the inconvenience.";
"Conversation.StoryForwardTooltip.TwoChats.One" = "Story forwarded to to **%@** and **%@**";
"Conversation.StoryForwardTooltip.ManyChats.One" = "Story forwarded to to **%@** and %@ others";
"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 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 makeMyStoriesController(context: AccountContext, isArchive: Bool) -> ViewController
func navigateToChatController(_ params: NavigateToChatControllerParams)
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>

View File

@ -49,8 +49,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
public let topicAuthorId: EnginePeer.Id?
public let hasBots: Bool
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.automaticDownloadPeerId = automaticDownloadPeerId
self.automaticDownloadNetworkType = automaticDownloadNetworkType
@ -72,6 +73,7 @@ public final class ChatMessageItemAssociatedData: Equatable {
self.alwaysDisplayTranscribeButton = alwaysDisplayTranscribeButton
self.hasBots = hasBots
self.translateToLanguage = translateToLanguage
self.maxReadStoryId = maxReadStoryId
}
public static func == (lhs: ChatMessageItemAssociatedData, rhs: ChatMessageItemAssociatedData) -> Bool {
@ -135,6 +137,9 @@ public final class ChatMessageItemAssociatedData: Equatable {
if lhs.translateToLanguage != rhs.translateToLanguage {
return false
}
if lhs.maxReadStoryId != rhs.maxReadStoryId {
return false
}
return true
}
}

View File

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

View File

@ -47,6 +47,7 @@ import InviteLinksUI
import ChatFolderLinkPreviewScreen
import StoryContainerScreen
import FullScreenEffectView
import PeerInfoStoryGridScreen
private final class ContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
@ -2604,6 +2605,30 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
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 {
items.append(.action(ContextMenuActionItem(text: "Send Message", icon: { theme in
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)"
case let dice as TelegramMediaDice:
messageText = dice.emoji
case _ as TelegramMediaStory:
//TODO:localize
messageText = "Story"
case let story as TelegramMediaStory:
if story.isMention, let peer {
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:
break
}

View File

@ -21,7 +21,7 @@ open class TransformImageNode: ASDisplayNode {
private var disposable = MetaDisposable()
private var currentTransform: ((TransformImageArguments) -> DrawingContext?)?
private var currentArguments: TransformImageArguments?
public private(set) var currentArguments: TransformImageArguments?
public private(set) var image: UIImage?
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> {
let signal = chatMessageVideoDatas(postbox: account.postbox, userLocation: userLocation, fileReference: fileReference, thumbnailSize: true, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred)
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, synchronousLoad: synchronousLoads, autoFetchFullSizeThumbnail: true, forceThumbnail: blurred)
return signal
|> 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)
if addAssociatedMessages {
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] = []
let notificationSettings: MessageNotificationSettings
@ -333,7 +333,7 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
entries.append(.exceptionsHeader(presentationData.theme, presentationData.strings.Notifications_MessageNotificationsExceptions.uppercased()))
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 rhsName = rhs.value.peer.displayTitle(strings: presentationData.strings, displayOrder: presentationData.nameDisplayOrder)
@ -355,99 +355,114 @@ private func notificationsPeerCategoryEntries(category: NotificationsPeerCategor
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 index: Int = 0
for (_, value) in sortedExceptions {
if !value.peer.isDeleted {
var title: String
var title: String = ""
if case .stories = category {
var muted = false
if value.settings.storySettings.mute == .muted {
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"
}
}
}
if automaticSet.contains(value.peer.id) {
//TODO:localize
title = "\(presentationData.strings.Notification_Exceptions_AlwaysOn) (automatic)"
} else {
var muted = false
switch value.settings.muteState {
case let .muted(until):
if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 {
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
}
if case .stories = category {
var muted = false
if value.settings.storySettings.mute == .muted {
muted = true
title.append(presentationData.strings.Notification_Exceptions_AlwaysOff)
} else {
title = presentationData.strings.Notification_Exceptions_AlwaysOn
title.append(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(", ")
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"
}
}
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 {
var muted = false
switch value.settings.muteState {
case let .muted(until):
if until >= Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) {
if until < Int32.max - 1 {
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 {
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 preferences = context.account.postbox.preferencesView(keys: [PreferencesKeys.globalNotifications])
let signal = combineLatest(context.sharedContext.presentationData, context.engine.peers.notificationSoundList(), sharedData, preferences, statePromise.get())
|> map { presentationData, notificationSoundList, sharedData, view, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
var automaticData: Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.NotificationSettings]), NoError> = .single(([], [:]))
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
if let settings = view.values[PreferencesKeys.globalNotifications]?.get(GlobalNotificationSettings.self) {
viewSettings = settings.effective
@ -932,7 +974,7 @@ public func notificationsPeerCategoryController(context: AccountContext, categor
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 scrollToItem: ListViewScrollToItem?

View File

@ -23,6 +23,7 @@ public protocol SparseItemGridView: UIView {
public protocol SparseItemGridDisplayItem: AnyObject {
var layer: SparseItemGridLayer? { get }
var view: SparseItemGridView? { get }
var blurLayer: SimpleLayer? { get }
}
public protocol SparseItemGridShimmerLayer: CALayer {
@ -342,6 +343,7 @@ public final class SparseItemGrid: ASDisplayNode {
let layer: SparseItemGridLayer?
let view: SparseItemGridView?
var shimmerLayer: SparseItemGridShimmerLayer?
var blurLayer: SimpleLayer?
init(layer: SparseItemGridLayer?, view: SparseItemGridView?) {
self.layer = layer
@ -391,8 +393,9 @@ public final class SparseItemGrid: ASDisplayNode {
let itemSpacing: CGFloat
let lastItemSize: CGFloat
let itemsPerRow: Int
let centerItems: Bool
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel) {
init(containerLayout: ContainerLayout, zoomLevel: ZoomLevel, itemCount: Int) {
self.containerLayout = containerLayout
let width: CGFloat
if containerLayout.useSideInsets {
@ -400,15 +403,23 @@ public final class SparseItemGrid: ASDisplayNode {
} else {
width = containerLayout.size.width
}
var centerItems = false
if let fixedItemHeight = containerLayout.fixedItemHeight {
self.itemsPerRow = 1
self.itemSize = CGSize(width: width, height: fixedItemHeight)
self.lastItemSize = width
self.itemSpacing = 0.0
self.centerItems = false
} else {
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)
let itemSize = floorToScreenPixels((width - (self.itemSpacing * CGFloat(self.itemsPerRow - 1))) / itemsPerRow)
if let fixedItemAspect = containerLayout.fixedItemAspect {
@ -417,16 +428,24 @@ public final class SparseItemGrid: ASDisplayNode {
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
}
}
func frame(at index: Int) -> CGRect {
let row = index / self.itemsPerRow
let column = index % self.itemsPerRow
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))
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))
if self.centerItems {
frame.origin.x = floor((self.containerLayout.size.width - frame.width) * 0.5)
}
return frame
}
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) {
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.updateVisibleItems(resetScrolling: true, synchronous: synchronous, restoreScrollPosition: restoreScrollPosition)
@ -949,6 +968,8 @@ public final class SparseItemGrid: ASDisplayNode {
var bindItems: [Item] = []
var bindLayers: [SparseItemGridDisplayItem] = []
var updateLayers: [SparseItemGridDisplayItem] = []
let addBlur = layout.centerItems
let visibleRange = layout.visibleItemRange(for: visibleBounds, count: items.count)
if visibleRange.maxIndex >= visibleRange.minIndex {
@ -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 {
let placeholderLayer: SparseItemGridShimmerLayer
if let current = itemLayer.shimmerLayer {
@ -995,6 +1032,9 @@ public final class SparseItemGrid: ASDisplayNode {
validIds.insert(item.id)
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 {
let placeholderLayer: SparseItemGridShimmerLayer
if self.visiblePlaceholders.count > usedPlaceholderCount {
@ -1022,7 +1062,7 @@ public final class SparseItemGrid: ASDisplayNode {
if let layer = item.layer {
layer.update(size: layer.frame.size)
} 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[-1445536993] = { return Api.UrlAuthResult.parse_urlAuthResultDefault($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[1340198022] = { return Api.UserFull.parse_userFull($0) }
dict[-2100168954] = { return Api.UserProfilePhoto.parse_userProfilePhoto($0) }

View File

@ -452,14 +452,14 @@ public extension Api {
}
public extension Api {
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)
public func serialize(_ buffer: Buffer, _ boxed: Swift.Bool) {
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 {
buffer.appendInt32(-1885878744)
buffer.appendInt32(-1414139616)
}
serializeInt32(flags, buffer: buffer, boxed: false)
serializeInt32(flags2, buffer: buffer, boxed: false)
@ -485,6 +485,7 @@ public extension Api {
for item in usernames! {
item.serialize(buffer, true)
}}
if Int(flags2) & Int(1 << 5) != 0 {serializeInt32(storiesMaxId!, buffer: buffer, boxed: false)}
break
case .userEmpty(let id):
if boxed {
@ -497,8 +498,8 @@ public extension Api {
public func descriptionFields() -> (String, [(String, Any)]) {
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):
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)])
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), ("storiesMaxId", storiesMaxId as Any)])
case .userEmpty(let id):
return ("userEmpty", [("id", id as Any)])
}
@ -547,6 +548,8 @@ public extension Api {
if Int(_2!) & Int(1 << 0) != 0 {if let _ = reader.readInt32() {
_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 _c2 = _2 != nil
let _c3 = _3 != nil
@ -563,8 +566,9 @@ public extension Api {
let _c14 = (Int(_1!) & Int(1 << 22) == 0) || _14 != nil
let _c15 = (Int(_1!) & Int(1 << 30) == 0) || _15 != 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 {
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)
let _c17 = (Int(_2!) & Int(1 << 5) == 0) || _17 != nil
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 {
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 {
static func getAllStories(flags: Int32, state: String?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.stories.AllStories>) {
let buffer = Buffer()

View File

@ -525,7 +525,7 @@ struct AccountMutableState {
var presences: [PeerId: Api.UserStatus] = [:]
for user in users {
switch user {
case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _):
case let .user(_, _, id, _, _, _, _, _, _, status, _, _, _, _, _, _, _):
if let status = 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):
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil)
case let .messageMediaStory(_, userId, id, _):
return (TelegramMediaStory(storyId: StoryId(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(userId)), id: id)), nil, nil, nil)
case let .messageMediaStory(flags, userId, id, _):
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)
for attribute in attributes {
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 {
convenience init(user: Api.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 isMin = (flags & (1 << 20)) != 0
@ -104,7 +104,7 @@ extension TelegramUser {
static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? {
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
if !isMin {
return TelegramUser(user: rhs)

View File

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

View File

@ -192,7 +192,7 @@ extension Api.Chat {
extension Api.User {
var peerId: PeerId {
switch self {
case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .user(_, _, id, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
return PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(id))
case let .userEmpty(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 storyId: StoryId
public let isMention: Bool
public var storyIds: [StoryId] {
return [self.storyId]
}
public init(storyId: StoryId) {
public init(storyId: StoryId, isMention: Bool) {
self.storyId = storyId
self.isMention = isMention
self.peerIds = [self.storyId.peerId]
}
public init(decoder: PostboxDecoder) {
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]
}
@ -27,6 +30,7 @@ public final class TelegramMediaStory: Media, Equatable {
public func encode(_ encoder: PostboxEncoder) {
encoder.encodeInt64(self.storyId.peerId.toInt64(), forKey: "pid")
encoder.encodeInt32(self.storyId.id, forKey: "sid")
encoder.encodeBool(self.isMention, forKey: "mns")
}
public func isLikelyToBeUpdated() -> Bool {
@ -48,6 +52,9 @@ public final class TelegramMediaStory: Media, Equatable {
if lhs.storyId != rhs.storyId {
return false
}
if lhs.isMention != rhs.isMention {
return false
}
return true
}

View File

@ -126,6 +126,7 @@ public enum Stories {
case isPublic
case isCloseFriends
case isForwardingDisabled
case isEdited
}
public let id: Int32
@ -141,6 +142,7 @@ public enum Stories {
public let isPublic: Bool
public let isCloseFriends: Bool
public let isForwardingDisabled: Bool
public let isEdited: Bool
public init(
id: Int32,
@ -155,7 +157,8 @@ public enum Stories {
isExpired: Bool,
isPublic: Bool,
isCloseFriends: Bool,
isForwardingDisabled: Bool
isForwardingDisabled: Bool,
isEdited: Bool
) {
self.id = id
self.timestamp = timestamp
@ -170,6 +173,7 @@ public enum Stories {
self.isPublic = isPublic
self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
}
public init(from decoder: Decoder) throws {
@ -194,6 +198,7 @@ public enum Stories {
self.isPublic = try container.decodeIfPresent(Bool.self, forKey: .isPublic) ?? false
self.isCloseFriends = try container.decodeIfPresent(Bool.self, forKey: .isCloseFriends) ?? 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 {
@ -219,6 +224,7 @@ public enum Stories {
try container.encode(self.isPublic, forKey: .isPublic)
try container.encode(self.isCloseFriends, forKey: .isCloseFriends)
try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled)
try container.encode(self.isEdited, forKey: .isEdited)
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
@ -269,6 +275,9 @@ public enum Stories {
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
if lhs.isEdited != rhs.isEdited {
return false
}
return true
}
@ -850,7 +859,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp))
@ -999,7 +1009,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
transaction.setStory(id: storyId, value: entry)
@ -1022,7 +1033,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
@ -1154,7 +1166,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) {
items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp)
@ -1176,7 +1189,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
updatedItems.append(updatedItem)
}
@ -1273,6 +1287,7 @@ extension Stories.StoredItem {
let isPublic = (flags & (1 << 7)) != 0
let isCloseFriends = (flags & (1 << 8)) != 0
let isForwardingDisabled = (flags & (1 << 10)) != 0
let isEdited = (flags & (1 << 11)) != 0
let item = Stories.Item(
id: id,
@ -1287,7 +1302,8 @@ extension Stories.StoredItem {
isExpired: isExpired,
isPublic: isPublic,
isCloseFriends: isCloseFriends,
isForwardingDisabled: isForwardingDisabled
isForwardingDisabled: isForwardingDisabled,
isEdited: isEdited
)
self = .item(item)
} else {

View File

@ -45,8 +45,9 @@ public final class EngineStoryItem: Equatable {
public let isPending: Bool
public let isCloseFriends: 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.timestamp = timestamp
self.expirationTimestamp = expirationTimestamp
@ -61,6 +62,7 @@ public final class EngineStoryItem: Equatable {
self.isPending = isPending
self.isCloseFriends = isCloseFriends
self.isForwardingDisabled = isForwardingDisabled
self.isEdited = isEdited
}
public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool {
@ -106,6 +108,9 @@ public final class EngineStoryItem: Equatable {
if lhs.isForwardingDisabled != rhs.isForwardingDisabled {
return false
}
if lhs.isEdited != rhs.isEdited {
return false
}
return true
}
}
@ -135,7 +140,8 @@ extension EngineStoryItem {
isExpired: self.isExpired,
isPublic: self.isPublic,
isCloseFriends: self.isCloseFriends,
isForwardingDisabled: self.isForwardingDisabled
isForwardingDisabled: self.isForwardingDisabled,
isEdited: self.isEdited
)
}
}
@ -494,7 +500,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
items.append(mappedItem)
}
@ -601,7 +608,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
storyItems.append(mappedItem)
}
@ -735,7 +743,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
finalUpdatedState = updatedState
}
@ -774,7 +783,8 @@ public final class PeerStoryListContext {
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
))
updatedState.items.sort(by: { lhs, rhs in
return lhs.timestamp > rhs.timestamp
@ -922,7 +932,8 @@ public final class PeerExpiringStoryListContext {
isPublic: item.isPublic,
isPending: false,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
)
items.append(.item(mappedItem))
}

View File

@ -965,7 +965,8 @@ public extension TelegramEngine {
isExpired: item.isExpired,
isPublic: item.isPublic,
isCloseFriends: item.isCloseFriends,
isForwardingDisabled: item.isForwardingDisabled
isForwardingDisabled: item.isForwardingDisabled,
isEdited: item.isEdited
))
if let entry = CodableEntry(updatedItem) {
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
default:
switch user {
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
let isMin = (flags & (1 << 20)) != 0
if isMin, let _ = transaction.getPeerPresence(peerId: peerId) {
} else {
@ -215,7 +215,7 @@ func updateContacts(transaction: Transaction, apiUsers: [Api.User]) {
for user in apiUsers {
var isContact: Bool?
switch user {
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
case let .user(flags, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _):
if (flags & (1 << 20)) == 0 {
isContact = (flags & (1 << 11)) != 0
}

View File

@ -1249,33 +1249,23 @@ public struct PresentationResourcesChat {
public static func chatExpiredStoryIndicatorIcon(_ theme: PresentationTheme, type: ChatExpiredStoryIndicatorType) -> UIImage? {
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.addPath(UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size), cornerRadius: 6.0).cgPath)
context.clip()
let color: UIColor
let foregroundColor: UIColor
switch type {
case .incoming:
color = theme.chat.message.incoming.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.incoming.mediaActiveControlColor
case .outgoing:
color = theme.chat.message.outgoing.mediaActiveControlColor.withMultipliedAlpha(0.1)
foregroundColor = theme.chat.message.outgoing.mediaActiveControlColor
case .free:
color = theme.chat.message.freeform.withWallpaper.reactionActiveMediaPlaceholder
foregroundColor = theme.chat.message.freeform.withWallpaper.reactionActiveBackground
foregroundColor = theme.chat.serviceMessage.components.withDefaultWallpaper.primaryText
}
context.setFillColor(color.cgColor)
context.fill(CGRect(origin: CGPoint(), size: size))
if let image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Message/ExpiredStoryIcon"), color: foregroundColor) {
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()
}

View File

@ -900,6 +900,16 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
case .file:
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 {
infoWidth += 32.0
}
var leftOffset: CGFloat = 0.0
if let storyData, storyData.isExpired {
leftOffset += 34.0 + 6.0
}
let leftOffset: CGFloat = 0.0
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
let node: ChatMessageForwardInfoNode
@ -290,8 +292,9 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
}
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(presentationData.theme.theme, type: imageType)
if let image = expiredStoryIconView.image {
expiredStoryIconView.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: image.size)
if let _ = expiredStoryIconView.image {
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 {
expiredStoryIconView.removeFromSuperview()

View File

@ -14,7 +14,7 @@ public final class EmptyStateIndicatorComponent: Component {
public let animationName: String
public let title: String
public let text: String
public let actionTitle: String
public let actionTitle: String?
public let action: () -> Void
public init(
@ -23,7 +23,7 @@ public final class EmptyStateIndicatorComponent: Component {
animationName: String,
title: String,
text: String,
actionTitle: String,
actionTitle: String?,
action: @escaping () -> Void
) {
self.context = context
@ -64,7 +64,7 @@ public final class EmptyStateIndicatorComponent: Component {
private let animation = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let text = ComponentView<Empty>()
private let button = ComponentView<Empty>()
private var button: ComponentView<Empty>?
override public init(frame: CGRect) {
super.init(frame: frame)
@ -108,35 +108,54 @@ public final class EmptyStateIndicatorComponent: Component {
environment: {},
containerSize: CGSize(width: min(300.0, availableSize.width - 16.0 * 2.0), height: 1000.0)
)
let buttonSize = self.button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
foreground: component.theme.list.itemCheckColors.foregroundColor,
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(id: 0, component: AnyComponent(
Text(text: component.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
var buttonSize: CGSize?
if let actionTitle = component.actionTitle {
let button: ComponentView<Empty>
if let current = self.button {
button = current
} else {
button = ComponentView()
self.button = button
}
buttonSize = button.update(
transition: transition,
component: AnyComponent(ButtonComponent(
background: ButtonComponent.Background(
color: component.theme.list.itemCheckColors.fillColor,
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 titleSpacing: 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)
@ -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))
contentY += textSize.height + buttonSpacing
}
if let buttonView = self.button.view {
if let buttonSize, let buttonView = self.button?.view {
if buttonView.superview == nil {
self.addSubview(buttonView)
}

View File

@ -716,7 +716,7 @@ private func notificationPeerExceptionEntries(presentationData: PresentationData
index += 1
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
entries.append(.showSender(index: index, theme: presentationData.theme, strings: presentationData.strings, value: .alwaysOn, selected: state.storiesHideSender == .alwaysOn))
index += 1

View File

@ -631,7 +631,7 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
}
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 {
layer.setContents(image)
switch synchronous {
@ -648,6 +648,11 @@ private final class SparseItemGridBindingImpl: SparseItemGridBinding {
}
if let image = result.blurredImage {
layer.setSpoilerContents(image)
if let blurLayer = displayItem.blurLayer {
blurLayer.contentsGravity = .resizeAspectFill
blurLayer.contents = result.blurredImage?.cgImage
}
}
if let loadSignal = result.loadSignal {
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?
if strongSelf.isArchive {
if strongSelf.isArchive && !mappedItems.isEmpty {
//TODO:localize
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)))
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>
var emptyStateTransition = Transition(transition)
if let current = self.emptyStateView {
@ -1893,9 +1903,9 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
context: self.context,
theme: presentationData.theme,
animationName: "StoryListEmpty",
title: "No saved stories",
text: "Open the Archive to select stories you\nwant to be displayed in your profile.",
actionTitle: "Open Archive",
title: self.isArchive ? "No Archived Stories" : "No saved stories",
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: self.isArchive ? nil : "Open Archive",
action: { [weak self] in
guard let self else {
return
@ -1948,10 +1958,11 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
fixedItemHeight = nil
}
let fixedItemAspect: CGFloat? = 9.0 / 16.0
let fixedItemAspect: CGFloat? = 0.72
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)
}
}

View File

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

View File

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

View File

@ -10,17 +10,15 @@ final class StoryAuthorInfoComponent: Component {
let context: AccountContext
let peer: EnginePeer?
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.peer = peer
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 {
if lhs.context !== rhs.context {
return false
@ -30,6 +28,9 @@ final class StoryAuthorInfoComponent: Component {
}
if lhs.timestamp != rhs.timestamp {
return false
}
if lhs.isEdited != rhs.isEdited {
return false
}
return true
}
@ -69,7 +70,12 @@ final class StoryAuthorInfoComponent: Component {
}
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(
transition: .immediate,

View File

@ -139,9 +139,11 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: item.isPublic,
isPending: false,
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) {
for item in localState.items {
mappedItems.append(EngineStoryItem(
@ -158,8 +160,10 @@ public final class StoryContentContextImpl: StoryContentContext {
isPublic: false,
isPending: true,
isCloseFriends: false,
isForwardingDisabled: false
isForwardingDisabled: false,
isEdited: false
))
totalCount += 1
}
}
@ -263,7 +267,7 @@ public final class StoryContentContextImpl: StoryContentContext {
peerId: peer.id,
storyItem: mappedItem
),
totalCount: mappedItems.count,
totalCount: totalCount,
previousItemId: previousItemId,
nextItemId: nextItemId,
allItems: allItems
@ -982,7 +986,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
isPublic: itemValue.isPublic,
isPending: false,
isCloseFriends: itemValue.isCloseFriends,
isForwardingDisabled: itemValue.isForwardingDisabled
isForwardingDisabled: itemValue.isForwardingDisabled,
isEdited: itemValue.isEdited
)
let mainItem = StoryContentItem(

View File

@ -516,8 +516,10 @@ private final class StoryContainerScreenComponent: Component {
}
if subview is ItemSetView {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
if let component = self.component, let stateValue = component.content.stateValue, let slice = stateValue.slice, let itemSetView = self.visibleItemSetViews[slice.peer.id], itemSetView === subview {
if let result = subview.hitTest(self.convert(point, to: subview), with: event) {
return result
}
}
} else {
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 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 {
environment.controller()?.dismiss()
} else {

View File

@ -217,7 +217,7 @@ final class StoryContentCaptionComponent: Component {
let edgeDistanceFraction = edgeDistance / 7.0
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))
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)))
@ -364,7 +364,9 @@ final class StoryContentCaptionComponent: Component {
attributedString: attributedText,
maximumNumberOfLines: 0,
truncationType: .end,
constrainedSize: textContainerSize
constrainedSize: textContainerSize,
textShadowColor: UIColor(white: 0.0, alpha: 0.25),
textShadowBlur: 4.0
))
let maxHeight: CGFloat = 50.0

View File

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

View File

@ -1772,7 +1772,7 @@ public final class StoryItemSetContainerComponent: Component {
guard let self else {
return
}
self.navigateToPeer(peer: peer)
self.navigateToPeer(peer: peer, chat: false)
}
)),
environment: {},
@ -2082,7 +2082,7 @@ public final class StoryItemSetContainerComponent: Component {
var currentCenterInfoItem: InfoItem?
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 {
currentCenterInfoItem = centerInfoItem
} else {
@ -2106,7 +2106,11 @@ public final class StoryItemSetContainerComponent: Component {
guard let self, let component = self.component else {
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: {},
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 {
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: {},
containerSize: CGSize(width: 32.0, height: 32.0)
@ -2433,7 +2441,7 @@ public final class StoryItemSetContainerComponent: Component {
presentationData: presentationData,
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 {
self.navigateToPeer(peer: peer, messageId: messageId)
self.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
}),
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 {
return
}
@ -2718,8 +2726,34 @@ public final class StoryItemSetContainerComponent: Component {
guard let navigationController = controller.navigationController as? NavigationController else {
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 {
return
}

View File

@ -344,7 +344,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId)
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
@ -386,7 +386,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId)
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
@ -448,7 +448,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId)
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}
@ -698,7 +698,7 @@ final class StoryItemSetContainerSendMessage {
let shareController = ShareController(
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,
externalShare: false,
immediateExternalShare: false,
@ -2134,7 +2134,7 @@ final class StoryItemSetContainerSendMessage {
animateInAsReplacement: false,
action: { [weak view] action in
if case .undo = action, let messageId = messageIds.first {
view?.navigateToPeer(peer: peer, messageId: messageId)
view?.navigateToPeer(peer: peer, chat: true, messageId: messageId)
}
return false
}

View File

@ -447,8 +447,9 @@ public final class StoryPeerListComponent: Component {
itemView.updateIsPreviewing(isPreviewing: peerId == itemId)
if component.unlocked && peerId == itemId {
if !self.scrollView.bounds.intersects(itemView.frame.insetBy(dx: 20.0, dy: 0.0)) {
self.scrollView.scrollRectToVisible(itemView.frame.insetBy(dx: -40.0, dy: 0.0), animated: false)
let itemFrame = itemView.frame.offsetBy(dx: self.scrollView.bounds.minX, dy: 0.0)
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
}
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
}

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" : [
{
"filename" : "timer.pdf",
"filename" : "ic_exipred.pdf",
"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 automaticMediaDownloadPeerType: MediaAutoDownloadPeerType = .channel
var contactsPeerIds: Set<PeerId> = Set()
@ -372,7 +372,7 @@ private func extractAssociatedData(chatLocation: ChatLocation, view: MessageHist
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 {
@ -1086,6 +1086,24 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
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,
historyViewUpdate,
self.chatPresentationDataPromise.get(),
@ -1103,8 +1121,9 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
promises,
topicAuthorId,
self.allAdMessagesPromise.get(),
translationState
).start(next: { [weak self] update, chatPresentationData, selectedMessages, updatingMedia, networkType, animatedEmojiStickers, additionalAnimatedEmojiStickers, customChannelDiscussionReadState, customThreadOutgoingReadState, availableReactions, defaultReaction, accountPeer, suggestAudioTranscription, promises, topicAuthorId, allAdMessages, translationState in
translationState,
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
func applyHole() {
@ -1260,7 +1279,7 @@ public final class ChatHistoryListNode: ListView, ChatHistoryNode {
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(
location: chatLocation,

View File

@ -38,6 +38,8 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
private var videoContent: NativeVideoContent?
private var videoStartTimestamp: Double?
private let fetchDisposable = MetaDisposable()
private var leadingIconView: UIImageView?
private var cachedMaskBackgroundImage: (CGPoint, UIImage, [CGRect])?
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)
var image: TelegramMediaImage?
var story: TelegramMediaStory?
for media in item.message.media {
if let action = media as? TelegramMediaAction {
switch action.action {
@ -175,12 +178,21 @@ class ChatMessageActionBubbleContentNode: ChatMessageBubbleContentNode {
default:
break
}
} else if let media = media as? TelegramMediaStory {
story = media
}
}
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()
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)
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.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)))
} else if let story = media as? TelegramMediaStory {
if let forwardInfo = message.forwardInfo, forwardInfo.flags.contains(.isImported), message.text.isEmpty {
messageWithCaptionToAdd = (message, itemAttributes)
}
if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty {
if story.isMention {
if let storyItem = message.associatedStories[story.storyId], storyItem.data.isEmpty {
result.append((message, ChatMessageActionBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
} else {
result.append((message, ChatMessageStoryMentionContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .freeform, neighborSpacing: .default)))
}
} 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 {
let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil)
@ -229,7 +234,14 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([
inner: for media in message.media {
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)))
needReactions = false
}

View File

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

View File

@ -137,13 +137,14 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
titleString = arguments.strings.User_DeletedAccount
}
//TODO:localize
isMedia = true
isText = false
if let storyItem = arguments.parentMessage.associatedStories[story], storyItem.data.isEmpty {
isExpiredStory = true
textString = NSAttributedString(string: "Expired story")
isMedia = false
} else {
textString = NSAttributedString(string: "Story")
isMedia = true
}
} else {
titleString = " "
@ -187,7 +188,7 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
case let .bubble(incoming):
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)
if isMedia {
if isMedia || isExpiredStory {
textColor = incoming ? arguments.presentationData.theme.theme.chat.message.incoming.secondaryTextColor : arguments.presentationData.theme.theme.chat.message.outgoing.secondaryTextColor
} else {
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
}
}
} 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 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 (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 imageSide: CGFloat
if isExpiredStory {
imageSide = 38.0
} else {
imageSide = titleLayout.size.height + textLayout.size.height - 16.0
}
imageSide = titleLayout.size.height + textLayout.size.height - 16.0
var applyImage: (() -> TransformImageNode)?
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
let node: ChatMessageReplyInfoNode
@ -414,6 +414,11 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
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 {
let expiredStoryIconView: UIImageView
if let current = node.expiredStoryIconView {
@ -434,17 +439,13 @@ public class ChatMessageReplyInfoNode: ASDisplayNode {
expiredStoryIconView.image = PresentationResourcesChat.chatExpiredStoryIndicatorIcon(arguments.presentationData.theme.theme, type: imageType)
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 {
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 {
let dustNode: InvisibleInkDustNode
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)
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(
sourceView: selectedTransitionNode.0.view,
sourceRect: selectedTransitionNode.1,
sourceCornerRadius: 0.0,
sourceCornerRadius: cornerRadius,
sourceIsAvatar: false
)
}
@ -67,6 +71,11 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
var selectedTransitionNode: (ASDisplayNode, CGRect, () -> (UIView?, UIView?))?
selectedTransitionNode = params.transitionNode(params.message.id, story, true)
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(
destinationView: selectedTransitionNode.0.view,
transitionView: StoryContainerScreen.TransitionView(
@ -83,20 +92,23 @@ func openChatMessageImpl(_ params: OpenChatMessageParams) -> Bool {
return
}
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 fromScale: CGFloat = 1.0
let scale = toScale.interpolate(to: fromScale, amount: state.progress)
transition.setTransform(view: view, transform: CATransform3DMakeScale(scale, scale, 1.0))
let toScaleX = state.sourceSize.width / state.destinationSize.width
let toScaleY = state.sourceSize.height / state.destinationSize.height
let fromScaleX: CGFloat = 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
params.addToTransitionSurface(view)
}
),
destinationRect: selectedTransitionNode.1,
destinationCornerRadius: 0.0,
destinationCornerRadius: cornerRadius,
destinationIsAvatar: false,
completed: {
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
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)
}

View File

@ -38,6 +38,7 @@ import ChatTextLinkEditUI
import AttachmentTextInputPanelNode
import ChatEntityKeyboardInputNode
import HashtagSearchUI
import PeerInfoStoryGridScreen
private final class AccountUserInterfaceInUseContext {
let subscribers = Bag<(Bool) -> Void>()
@ -1723,6 +1724,10 @@ public final class SharedAccountContextImpl: SharedAccountContext {
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 {
let mappedSource: PremiumSource
switch source {