diff --git a/Telegram/NotificationService/Sources/NotificationService.swift b/Telegram/NotificationService/Sources/NotificationService.swift index a512d4008a..bad26abdb6 100644 --- a/Telegram/NotificationService/Sources/NotificationService.swift +++ b/Telegram/NotificationService/Sources/NotificationService.swift @@ -1171,6 +1171,7 @@ private final class NotificationServiceHandler { }*/ if let storyId { + content.category = "st" action = .pollStories(peerId: peerId, content: content, storyId: storyId) } else { action = .poll(peerId: peerId, content: content, messageId: messageIdValue) diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index bc4690c534..04c8b603b0 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9400,7 +9400,7 @@ Sorry for the inconvenience."; "StoryFeed.ContextSavedStories" = "Saved Stories"; "StoryFeed.ContextArchivedStories" = "Archived Stories"; "StoryFeed.ContextOpenChat" = "Send Message"; -"StoryFeed.ContextOpenProfile" = "View Profile"; +"StoryFeed.ContextOpenProfile" = "Open Profile"; "StoryFeed.ContextNotifyOn" = "Notify About Stories"; "StoryFeed.ContextNotifyOff" = "Do Not Notify About Stories"; "StoryFeed.ContextArchive" = "Hide Stories"; @@ -9452,6 +9452,7 @@ Sorry for the inconvenience."; "ArchiveSettings.UnknownChatsFooter" = "Automatically archive and mute new private chats, groups and channels from non-contacts."; "ArchiveSettings.KeepArchived" = "Always Keep Archived"; +"ArchiveSettings.AutomaticallyArchive" = "Automatically Archive"; "ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [Telegram Premium]()."; "NotificationSettings.Stories.ShowAll" = "Show All Notifications"; @@ -9548,10 +9549,9 @@ Sorry for the inconvenience."; "Story.ContextDeleteStory" = "Delete Story"; -"Story.TooltipPrivacyCloseFriends" = "You are seeing this story because you have\nbeen added to **%@'s** list of close friends."; +"Story.TooltipPrivacyCloseFriends" = "You are seeing this story because **%@** added you\nto their list of Close Friends."; "Story.TooltipPrivacyContacts" = "Only **%@'s** contacts can view this story."; "Story.TooltipPrivacySelectedContacts" = "Only some contacts **%@** selected can view this story."; - "Story.ToastViewInChat" = "View in Chat"; "Story.ToastReactionSent" = "Reaction Sent."; @@ -9618,7 +9618,7 @@ Sorry for the inconvenience."; "Story.Editor.DraftDiscardDraft" = "Discard Draft?"; "Story.Editor.DraftKeepMedia" = "Save Draft"; "Story.Editor.DraftKeepDraft" = "Keep Draft"; -"Story.Editor.DraftDiscaedText" = "If you go back now, you will lose any changes that you've made."; +"Story.Editor.DraftDiscaedText" = "If you go back now, you will lose any changes you made."; "Story.Editor.DraftDiscard" = "Discard"; "Story.Editor.ExpirationText" = "Choose how long the story will be visible."; @@ -9679,7 +9679,7 @@ Sorry for the inconvenience."; "Story.Privacy.TooltipStoryArchived" = "Users allowed to view your story will see it on your page even after it expires."; "Story.Privacy.TooltipStoryExpires" = "The story will disappear after it expires."; -"Story.Privacy.WhoCanViewHeader" = "WHO CAN VIEW"; +"Story.Privacy.WhoCanViewHeader" = "WHO CAN VIEW THIS STORY"; "Story.Privacy.ContactsHeader" = "CONTACTS"; "Story.Privacy.SearchChats" = "Search Chats"; @@ -9712,6 +9712,19 @@ Sorry for the inconvenience."; "Story.Privacy.SaveSettings" = "Save Settings"; "Story.Privacy.PostStory" = "Post Story"; -"Story.Editor.Draft" = "Draft"; "Story.Views.ViewsExpired" = "List of viewers becomes unavailable **24 hours** after the story expires."; "Story.Views.NoViews" = "Nobody has viewed\nyour story yet."; + +"AutoDownloadSettings.Stories" = "Stories"; +"MediaEditor.Draft" = "Draft"; + +"Notification.LockScreenStoryPlaceholder" = "New Story"; + +"Chat.OpenStory" = "OPEN STORY"; + +"Story.Editor.TooltipPremiumCaptionLimitTitle" = "Maximum Length Reached"; +"Story.Editor.TooltipPremiumCaptionLimitText" = "Increase this limit 10 times to 2048 symbols by subscribing to [Telegram Premium]()."; + +"Story.Editor.TooltipPremiumCaptionEntities" = "Subscribe to [Telegram Premium]() to add links and formatting in captions to your stories."; + +"Story.Context.TooltipPremiumSaveStories" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery."; diff --git a/submodules/AccountContext/Sources/UniversalVideoNode.swift b/submodules/AccountContext/Sources/UniversalVideoNode.swift index bea26c38aa..1828a29695 100644 --- a/submodules/AccountContext/Sources/UniversalVideoNode.swift +++ b/submodules/AccountContext/Sources/UniversalVideoNode.swift @@ -23,6 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject { func setSoundEnabled(_ value: Bool) func seek(_ timestamp: Double) func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) + func setSoundMuted(soundMuted: Bool) func continueWithOverridingAmbientMode(isAmbient: Bool) func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool) func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) @@ -284,6 +285,14 @@ public final class UniversalVideoNode: ASDisplayNode { }) } + public func setSoundMuted(soundMuted: Bool) { + self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in + if let contentNode = contentNode { + contentNode.setSoundMuted(soundMuted: soundMuted) + } + }) + } + public func continueWithOverridingAmbientMode(isAmbient: Bool) { self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in if let contentNode = contentNode { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 53b9c56dc3..89c2e05b0b 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -1856,101 +1856,49 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } }) - self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive)) - |> deliverOnMainQueue).start(next: { [weak self] rawStorySubscriptions in - guard let self else { - return - } - - self.rawStorySubscriptions = rawStorySubscriptions - var items: [EngineStorySubscriptions.Item] = [] - if self.shouldFixStorySubscriptionOrder { - for peerId in self.fixedStorySubscriptionOrder { - if let item = rawStorySubscriptions.items.first(where: { $0.peer.id == peerId }) { + if self.previewing { + self.storiesReady.set(.single(true)) + } else { + self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive)) + |> deliverOnMainQueue).start(next: { [weak self] rawStorySubscriptions in + guard let self else { + return + } + + self.rawStorySubscriptions = rawStorySubscriptions + var items: [EngineStorySubscriptions.Item] = [] + if self.shouldFixStorySubscriptionOrder { + for peerId in self.fixedStorySubscriptionOrder { + if let item = rawStorySubscriptions.items.first(where: { $0.peer.id == peerId }) { + items.append(item) + } + } + } + for item in rawStorySubscriptions.items { + if !items.contains(where: { $0.peer.id == item.peer.id }) { items.append(item) } } - } - for item in rawStorySubscriptions.items { - if !items.contains(where: { $0.peer.id == item.peer.id }) { - items.append(item) - } - } - self.orderedStorySubscriptions = EngineStorySubscriptions( - accountItem: rawStorySubscriptions.accountItem, - items: items, - hasMoreToken: rawStorySubscriptions.hasMoreToken - ) - self.fixedStorySubscriptionOrder = items.map(\.peer.id) - - let transition: ContainedViewLayoutTransition - if self.didAppear { - transition = .animated(duration: 0.4, curve: .spring) - } else { - transition = .immediate - } - - self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition - self.requestLayout(transition: transition) - self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil - - if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) { - self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded() - } - - self.storiesReady.set(.single(true)) - - Queue.mainQueue().after(1.0, { [weak self] in - guard let self else { - return - } - self.maybeDisplayStoryTooltip() - }) - }) - self.storyProgressDisposable = (self.context.engine.messages.allStoriesUploadProgress() - |> deliverOnMainQueue).start(next: { [weak self] progress in - guard let self else { - return - } - self.updateStoryUploadProgress(progress) - }) - - if case .chatList(.root) = self.location { - self.storyArchiveSubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: true) - |> deliverOnMainQueue).start(next: { [weak self] rawStoryArchiveSubscriptions in - guard let self else { - return - } + self.orderedStorySubscriptions = EngineStorySubscriptions( + accountItem: rawStorySubscriptions.accountItem, + items: items, + hasMoreToken: rawStorySubscriptions.hasMoreToken + ) + self.fixedStorySubscriptionOrder = items.map(\.peer.id) - self.rawStoryArchiveSubscriptions = rawStoryArchiveSubscriptions - - let archiveStoryState: ChatListNodeState.StoryState? - if rawStoryArchiveSubscriptions.items.isEmpty { - archiveStoryState = nil + let transition: ContainedViewLayoutTransition + if self.didAppear { + transition = .animated(duration: 0.4, curve: .spring) } else { - var unseenCount = 0 - for item in rawStoryArchiveSubscriptions.items { - if item.hasUnseen { - unseenCount += 1 - } - } - let hasUnseenCloseFriends = rawStoryArchiveSubscriptions.items.contains(where: { $0.hasUnseenCloseFriends }) - archiveStoryState = ChatListNodeState.StoryState( - stats: EngineChatList.StoryStats( - totalCount: rawStoryArchiveSubscriptions.items.count, - unseenCount: unseenCount, - hasUnseenCloseFriends: hasUnseenCloseFriends - ), - hasUnseenCloseFriends: hasUnseenCloseFriends - ) + transition = .immediate } - self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in - var chatListState = chatListState - - chatListState.archiveStoryState = archiveStoryState - - return chatListState + self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition + self.requestLayout(transition: transition) + self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil + + if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) { + self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded() } self.storiesReady.set(.single(true)) @@ -1961,9 +1909,65 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } self.maybeDisplayStoryTooltip() }) - - self.hasPendingStoriesPromise.set(rawStoryArchiveSubscriptions.accountItem?.hasPending ?? false) }) + self.storyProgressDisposable = (self.context.engine.messages.allStoriesUploadProgress() + |> deliverOnMainQueue).start(next: { [weak self] progress in + guard let self else { + return + } + self.updateStoryUploadProgress(progress) + }) + + if case .chatList(.root) = self.location { + self.storyArchiveSubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: true) + |> deliverOnMainQueue).start(next: { [weak self] rawStoryArchiveSubscriptions in + guard let self else { + return + } + + self.rawStoryArchiveSubscriptions = rawStoryArchiveSubscriptions + + let archiveStoryState: ChatListNodeState.StoryState? + if rawStoryArchiveSubscriptions.items.isEmpty { + archiveStoryState = nil + } else { + var unseenCount = 0 + for item in rawStoryArchiveSubscriptions.items { + if item.hasUnseen { + unseenCount += 1 + } + } + let hasUnseenCloseFriends = rawStoryArchiveSubscriptions.items.contains(where: { $0.hasUnseenCloseFriends }) + archiveStoryState = ChatListNodeState.StoryState( + stats: EngineChatList.StoryStats( + totalCount: rawStoryArchiveSubscriptions.items.count, + unseenCount: unseenCount, + hasUnseenCloseFriends: hasUnseenCloseFriends + ), + hasUnseenCloseFriends: hasUnseenCloseFriends + ) + } + + self.chatListDisplayNode.mainContainerNode.currentItemNode.updateState { chatListState in + var chatListState = chatListState + + chatListState.archiveStoryState = archiveStoryState + + return chatListState + } + + self.storiesReady.set(.single(true)) + + Queue.mainQueue().after(1.0, { [weak self] in + guard let self else { + return + } + self.maybeDisplayStoryTooltip() + }) + + self.hasPendingStoriesPromise.set(rawStoryArchiveSubscriptions.accountItem?.hasPending ?? false) + }) + } } } } diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index 0cecb062cb..d763c76347 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -2169,7 +2169,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return } - if let storySubscriptions = self.controller?.orderedStorySubscriptions { + if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { let _ = storySubscriptions self.tempAllowAvatarExpansion = true diff --git a/submodules/ChatListUI/Sources/Node/ChatListItem.swift b/submodules/ChatListUI/Sources/Node/ChatListItem.swift index 8b481e6c1b..bd7a1156a3 100644 --- a/submodules/ChatListUI/Sources/Node/ChatListItem.swift +++ b/submodules/ChatListUI/Sources/Node/ChatListItem.swift @@ -3601,6 +3601,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode { } else { strongSelf.view.accessibilityCustomActions = nil } + + strongSelf.avatarTapRecognizer?.isEnabled = item.interaction.inlineNavigationLocation == nil } }) } diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index 3a3abedbce..e1fdf727ac 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1261,7 +1261,7 @@ open class TextNode: ASDisplayNode { var additionalTrailingLine: (CTLine, Double)? var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine) - if customTruncationToken != nil { + if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length { measureFitWidth += truncationTokenWidth } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index 8deb0e25bf..f6b688fbbd 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -27,15 +27,15 @@ enum MediaPickerGridItemContent: Equatable { final class MediaPickerGridItem: GridItem { let content: MediaPickerGridItemContent let interaction: MediaPickerInteraction - let strings: PresentationStrings let theme: PresentationTheme + let strings: PresentationStrings let selectable: Bool let enableAnimations: Bool let stories: Bool let section: GridSection? = nil - init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, strings: PresentationStrings, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { + init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.content = content self.interaction = interaction self.strings = strings @@ -57,7 +57,7 @@ final class MediaPickerGridItem: GridItem { return node case let .draft(draft, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, draft: draft, index: index, strings: self.strings, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node } } @@ -73,7 +73,7 @@ final class MediaPickerGridItem: GridItem { case let .media(media, index): node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) case let .draft(draft, index): - node.setup(interaction: self.interaction, draft: draft, index: index, strings: self.strings, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, strings: self.strings, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) } } } @@ -289,7 +289,7 @@ final class MediaPickerGridItemNode: GridItemNode { } } - func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, strings: PresentationStrings, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { + func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, strings: PresentationStrings, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -312,7 +312,7 @@ final class MediaPickerGridItemNode: GridItemNode { } if self.draftNode.supernode == nil { - self.draftNode.attributedText = NSAttributedString(string: strings.Story_Editor_Draft, font: Font.semibold(12.0), textColor: .white) + self.draftNode.attributedText = NSAttributedString(string: strings.MediaEditor_Draft, font: Font.semibold(12.0), textColor: .white) self.addSubnode(self.draftNode) } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 09b0ec6a93..aa462a0bc7 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -61,8 +61,8 @@ private struct MediaPickerGridEntry: Comparable, Identifiable { return lhs.stableId < rhs.stableId } - func item(context: AccountContext, interaction: MediaPickerInteraction, strings: PresentationStrings, theme: PresentationTheme) -> MediaPickerGridItem { - return MediaPickerGridItem(content: self.content, interaction: interaction, strings: strings, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories) + func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings) -> MediaPickerGridItem { + return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, strings: strings, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories) } } @@ -72,12 +72,12 @@ private struct MediaPickerGridTransaction { let updates: [GridNodeUpdateItem] let scrollToItem: GridNodeScrollToItem? - init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], context: AccountContext, interaction: MediaPickerInteraction, strings: PresentationStrings, theme: PresentationTheme, scrollToItem: GridNodeScrollToItem?) { + init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, scrollToItem: GridNodeScrollToItem?) { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices - self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, strings: strings, theme: theme), previousIndex: $0.2) } - self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, strings: strings, theme: theme)) } + self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings), previousIndex: $0.2) } + self.updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings)) } self.scrollToItem = scrollToItem } @@ -671,7 +671,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { scrollToItem = GridNodeScrollToItem(index: entries.count - 1, position: .bottom(0.0), transition: .immediate, directionHint: .down, adjustForSection: false) } - let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, strings: self.presentationData.strings, theme: self.presentationData.theme, scrollToItem: scrollToItem) + let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, strings: self.presentationData.strings, scrollToItem: scrollToItem) self.enqueueTransaction(transaction) if !self.didSetReady { diff --git a/submodules/MediaPlayer/Sources/MediaPlayer.swift b/submodules/MediaPlayer/Sources/MediaPlayer.swift index 67e142d587..20ec8f5f5d 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayer.swift @@ -124,6 +124,7 @@ private final class MediaPlayerContext { private var baseRate: Double private let fetchAutomatically: Bool private var playAndRecord: Bool + private var soundMuted: Bool private var ambient: Bool private var mixWithOthers: Bool private var keepAudioSessionWhilePaused: Bool @@ -150,7 +151,7 @@ private final class MediaPlayerContext { private var stoppedAtEnd = false - init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { + init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise, audioLevelPipe: ValuePipe, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String?, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool, enableSound: Bool, baseRate: Double, fetchAutomatically: Bool, playAndRecord: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, keepAudioSessionWhilePaused: Bool, continuePlayingWithoutSoundOnLostAudioSession: Bool, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool) { assert(queue.isCurrent()) self.queue = queue @@ -169,6 +170,7 @@ private final class MediaPlayerContext { self.baseRate = baseRate self.fetchAutomatically = fetchAutomatically self.playAndRecord = playAndRecord + self.soundMuted = soundMuted self.ambient = ambient self.mixWithOthers = mixWithOthers self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused @@ -404,7 +406,7 @@ private final class MediaPlayerContext { self.audioRenderer = nil let queue = self.queue - renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), forAudioVideoMessage: self.isAudioVideoMessage, playAndRecord: self.playAndRecord, ambient: self.ambient, mixWithOthers: self.mixWithOthers, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in + renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), forAudioVideoMessage: self.isAudioVideoMessage, playAndRecord: self.playAndRecord, soundMuted: self.soundMuted, ambient: self.ambient, mixWithOthers: self.mixWithOthers, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in queue.async { if let strongSelf = self { strongSelf.tick() @@ -483,7 +485,7 @@ private final class MediaPlayerContext { self.lastStatusUpdateTimestamp = nil if self.enableSound { let queue = self.queue - let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), forAudioVideoMessage: self.isAudioVideoMessage, playAndRecord: self.playAndRecord, ambient: self.ambient, mixWithOthers: self.mixWithOthers, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in + let renderer = MediaPlayerAudioRenderer(audioSession: .manager(self.audioSessionManager), forAudioVideoMessage: self.isAudioVideoMessage, playAndRecord: self.playAndRecord, soundMuted: self.soundMuted, ambient: self.ambient, mixWithOthers: self.mixWithOthers, forceAudioToSpeaker: self.forceAudioToSpeaker, baseRate: self.baseRate, audioLevelPipe: self.audioLevelPipe, updatedRate: { [weak self] in queue.async { if let strongSelf = self { strongSelf.tick() @@ -601,43 +603,15 @@ private final class MediaPlayerContext { self.stoppedAtEnd = false } + fileprivate func setSoundMuted(soundMuted: Bool) { + self.soundMuted = soundMuted + self.audioRenderer?.renderer.setSoundMuted(soundMuted: soundMuted) + } + fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) { - if !isAmbient { - self.ambient = false - var loadedState: MediaPlayerLoadedState? - switch self.state { - case .empty: - break - case let .playing(currentLoadedState): - loadedState = currentLoadedState - case let .paused(currentLoadedState): - loadedState = currentLoadedState - case .seeking: - break - } - - if let loadedState = loadedState { - let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) - self.seek(timestamp: timestamp, action: .play) - } - } else { - self.ambient = true - var loadedState: MediaPlayerLoadedState? - switch self.state { - case .empty: - break - case let .playing(currentLoadedState): - loadedState = currentLoadedState - case let .paused(currentLoadedState): - loadedState = currentLoadedState - case .seeking: - break - } - - if let loadedState = loadedState { - let timestamp = CMTimeGetSeconds(CMTimebaseGetTime(loadedState.controlTimebase.timebase)) - self.seek(timestamp: timestamp, action: .play) - } + if self.ambient != isAmbient { + self.ambient = isAmbient + self.audioRenderer?.renderer.reconfigureAudio(ambient: self.ambient) } } @@ -1154,10 +1128,10 @@ public final class MediaPlayer { } } - public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { + public init(audioSessionManager: ManagedAudioSession, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, resourceReference: MediaResourceReference, tempFilePath: String? = nil, streamable: MediaPlayerStreaming, video: Bool, preferSoftwareDecoding: Bool, playAutomatically: Bool = false, enableSound: Bool, baseRate: Double = 1.0, fetchAutomatically: Bool, playAndRecord: Bool = false, soundMuted: Bool = false, ambient: Bool = false, mixWithOthers: Bool = false, keepAudioSessionWhilePaused: Bool = false, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, storeAfterDownload: (() -> Void)? = nil, isAudioVideoMessage: Bool = false) { let audioLevelPipe = self.audioLevelPipe self.queue.async { - let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + let context = MediaPlayerContext(queue: self.queue, audioSessionManager: audioSessionManager, playerStatus: self.statusValue, audioLevelPipe: audioLevelPipe, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: resourceReference, tempFilePath: tempFilePath, streamable: streamable, video: video, preferSoftwareDecoding: preferSoftwareDecoding, playAutomatically: playAutomatically, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, playAndRecord: playAndRecord, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, keepAudioSessionWhilePaused: keepAudioSessionWhilePaused, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) self.contextRef = Unmanaged.passRetained(context) } } @@ -1185,6 +1159,14 @@ public final class MediaPlayer { } } + public func setSoundMuted(soundMuted: Bool) { + self.queue.async { + if let context = self.contextRef?.takeUnretainedValue() { + context.setSoundMuted(soundMuted: soundMuted) + } + } + } + public func continueWithOverridingAmbientMode(isAmbient: Bool) { self.queue.async { if let context = self.contextRef?.takeUnretainedValue() { diff --git a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift index 5f2043188c..08be7b2aca 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerAudioRenderer.swift @@ -237,7 +237,9 @@ private final class AudioPlayerRendererContext { let audioSessionDisposable = MetaDisposable() var audioSessionControl: ManagedAudioSessionControl? let playAndRecord: Bool - let ambient: Bool + var soundMuted: Bool + var ambient: Bool + var volume: Double = 1.0 let mixWithOthers: Bool var forceAudioToSpeaker: Bool { didSet { @@ -252,7 +254,7 @@ private final class AudioPlayerRendererContext { } } - init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool, playAndRecord: Bool, useVoiceProcessingMode: Bool, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + init(controlTimebase: CMTimebase, audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool, playAndRecord: Bool, useVoiceProcessingMode: Bool, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { assert(audioPlayerRendererQueue.isCurrent()) self.audioSession = audioSession @@ -267,6 +269,7 @@ private final class AudioPlayerRendererContext { self.playAndRecord = playAndRecord self.useVoiceProcessingMode = useVoiceProcessingMode + self.soundMuted = soundMuted self.ambient = ambient self.mixWithOthers = mixWithOthers @@ -318,8 +321,10 @@ private final class AudioPlayerRendererContext { } fileprivate func setVolume(_ volume: Double) { + self.volume = volume + if let mixerAudioUnit = self.mixerAudioUnit { - AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(volume), 0) + AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(volume) * (self.soundMuted ? 0.0 : 1.0), 0) } } @@ -345,6 +350,36 @@ private final class AudioPlayerRendererContext { } } + fileprivate func setSoundMuted(soundMuted: Bool) { + self.soundMuted = soundMuted + + if let mixerAudioUnit = self.mixerAudioUnit { + AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(self.volume) * (self.soundMuted ? 0.0 : 1.0), 0) + } + } + + fileprivate func reconfigureAudio(ambient: Bool) { + self.ambient = ambient + + if let audioGraph = self.audioGraph { + var isRunning: DarwinBoolean = false + AUGraphIsRunning(audioGraph, &isRunning) + if isRunning.boolValue { + AUGraphStop(audioGraph) + } + } + self.audioSessionControl?.setType(self.ambient ? .ambient : (self.playAndRecord ? .playWithPossiblePortOverride : .play(mixWithOthers: self.mixWithOthers)), completion: { [weak self] in + audioPlayerRendererQueue.async { + guard let self else { + return + } + if let audioGraph = self.audioGraph { + AUGraphStart(audioGraph) + } + } + }) + } + fileprivate func flushBuffers(at timestamp: CMTime, completion: () -> Void) { assert(audioPlayerRendererQueue.isCurrent()) @@ -554,6 +589,8 @@ private final class AudioPlayerRendererContext { if self.forAudioVideoMessage && !self.ambient { AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, self.forceAudioToSpeaker ? 0.0 : 12.0, 0) + } else if self.soundMuted { + AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, 0.0, 0) } var maybeOutputAudioUnit: AudioComponentInstance? @@ -590,6 +627,8 @@ private final class AudioPlayerRendererContext { AudioUnitSetProperty(mixerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) AudioUnitSetProperty(equalizerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4) + + AudioUnitSetParameter(mixerAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, Float32(self.volume) * (self.soundMuted ? 0.0 : 1.0), 0) guard AUGraphInitialize(audioGraph) == noErr else { return @@ -827,7 +866,7 @@ public final class MediaPlayerAudioRenderer { private let audioClock: CMClock public let audioTimebase: CMTimebase - public init(audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool = false, playAndRecord: Bool, useVoiceProcessingMode: Bool = false, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { + public init(audioSession: MediaPlayerAudioSessionControl, forAudioVideoMessage: Bool = false, playAndRecord: Bool, useVoiceProcessingMode: Bool = false, soundMuted: Bool, ambient: Bool, mixWithOthers: Bool, forceAudioToSpeaker: Bool, baseRate: Double, audioLevelPipe: ValuePipe, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) { var audioClock: CMClock? CMAudioClockCreate(allocator: nil, clockOut: &audioClock) if audioClock == nil { @@ -840,7 +879,7 @@ public final class MediaPlayerAudioRenderer { self.audioTimebase = audioTimebase! audioPlayerRendererQueue.async { - let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, forAudioVideoMessage: forAudioVideoMessage, playAndRecord: playAndRecord, useVoiceProcessingMode: useVoiceProcessingMode, ambient: ambient, mixWithOthers: mixWithOthers, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused) + let context = AudioPlayerRendererContext(controlTimebase: audioTimebase!, audioSession: audioSession, forAudioVideoMessage: forAudioVideoMessage, playAndRecord: playAndRecord, useVoiceProcessingMode: useVoiceProcessingMode, soundMuted: soundMuted, ambient: ambient, mixWithOthers: mixWithOthers, forceAudioToSpeaker: forceAudioToSpeaker, baseRate: baseRate, audioLevelPipe: audioLevelPipe, updatedRate: updatedRate, audioPaused: audioPaused) self.contextRef = Unmanaged.passRetained(context) } } @@ -870,6 +909,24 @@ public final class MediaPlayerAudioRenderer { } } + public func setSoundMuted(soundMuted: Bool) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.setSoundMuted(soundMuted: soundMuted) + } + } + } + + public func reconfigureAudio(ambient: Bool) { + audioPlayerRendererQueue.async { + if let contextRef = self.contextRef { + let context = contextRef.takeUnretainedValue() + context.reconfigureAudio(ambient: ambient) + } + } + } + public func setRate(_ rate: Double) { audioPlayerRendererQueue.async { if let contextRef = self.contextRef { diff --git a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift index 1a35e1f3ee..33779e65da 100644 --- a/submodules/SettingsUI/Sources/ArchiveSettingsController.swift +++ b/submodules/SettingsUI/Sources/ArchiveSettingsController.swift @@ -105,7 +105,7 @@ private enum ArchiveSettingsControllerEntry: ItemListNodeEntry { case .unknownHeader: return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnknownChatsHeader, sectionId: self.section) case let .unknownValue(isOn, isLocked): - return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_KeepArchived, value: isOn, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { value in + return ItemListSwitchItem(presentationData: presentationData, title: presentationData.strings.ArchiveSettings_AutomaticallyArchive, value: isOn, enableInteractiveChanges: !isLocked, enabled: true, displayLocked: isLocked, sectionId: self.section, style: .blocks, updated: { value in arguments.updateUnknown(value) }, activatedWhileDisabled: { arguments.updateUnknown(nil) diff --git a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift index 2e293db310..b8d635f6be 100644 --- a/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift +++ b/submodules/SettingsUI/Sources/Data and Storage/AutodownloadConnectionTypeController.swift @@ -292,7 +292,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData: entries.append(.typesHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_MediaTypes)) entries.append(.photos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Photos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: photo, category: .photo), master)) - entries.append(.stories(presentationData.theme, "Stories", stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: stories, category: .story), master)) + entries.append(.stories(presentationData.theme, presentationData.strings.AutoDownloadSettings_Stories, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: stories, category: .story), master)) entries.append(.videos(presentationData.theme, presentationData.strings.AutoDownloadSettings_Videos, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: video, category: .video), master)) entries.append(.files(presentationData.theme, presentationData.strings.AutoDownloadSettings_Files, stringForAutomaticDownloadPeers(strings: presentationData.strings, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator, peers: file, category: .file), master)) entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo)) diff --git a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift index 19fd2c372e..b003061a4e 100644 --- a/submodules/SparseItemGrid/Sources/SparseItemGrid.swift +++ b/submodules/SparseItemGrid/Sources/SparseItemGrid.swift @@ -8,7 +8,7 @@ import ComponentFlow import MultilineTextComponent public protocol SparseItemGridLayer: CALayer { - func update(size: CGSize) + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) func needsShimmer() -> Bool func getContents() -> Any? @@ -967,7 +967,7 @@ public final class SparseItemGrid: ASDisplayNode { var bindItems: [Item] = [] var bindLayers: [SparseItemGridDisplayItem] = [] - var updateLayers: [SparseItemGridDisplayItem] = [] + var updateLayers: [(SparseItemGridDisplayItem, Int)] = [] let addBlur = layout.centerItems @@ -980,7 +980,7 @@ public final class SparseItemGrid: ASDisplayNode { let itemLayer: VisibleItem if let current = self.visibleItems[item.id] { itemLayer = current - updateLayers.append(itemLayer) + updateLayers.append((itemLayer, index)) } else { itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView()) self.visibleItems[item.id] = itemLayer @@ -1057,10 +1057,11 @@ public final class SparseItemGrid: ASDisplayNode { items.itemBinding.bindLayers(items: bindItems, layers: bindLayers, size: layout.containerLayout.size, insets: layout.containerLayout.insets, synchronous: synchronous) } - for item in updateLayers { + for (item, index) in updateLayers { let item = item as! VisibleItem + let contentItem = items.item(at: index) if let layer = item.layer { - layer.update(size: layer.frame.size) + layer.update(size: layer.frame.size, insets: layout.containerLayout.insets, displayItem: item, binding: items.itemBinding, item: contentItem) } else if let view = item.view { view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets) } diff --git a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift index a238e1e8d4..45d2b6e15f 100644 --- a/submodules/TelegramAudio/Sources/ManagedAudioSession.swift +++ b/submodules/TelegramAudio/Sources/ManagedAudioSession.swift @@ -120,7 +120,7 @@ public enum AudioSessionOutputMode: Equatable { private final class HolderRecord { let id: Int32 - let audioSessionType: ManagedAudioSessionType + var audioSessionType: ManagedAudioSessionType let control: ManagedAudioSessionControl let activate: (ManagedAudioSessionControl) -> Void let deactivate: (Bool) -> Signal @@ -161,12 +161,14 @@ public class ManagedAudioSessionControl { private let activateImpl: (ManagedAudioSessionControlActivate) -> Void private let setupAndActivateImpl: (Bool, ManagedAudioSessionControlActivate) -> Void private let setOutputModeImpl: (AudioSessionOutputMode) -> Void + private let setTypeImpl: (ManagedAudioSessionType, @escaping () -> Void) -> Void - fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void, setupAndActivateImpl: @escaping (Bool, ManagedAudioSessionControlActivate) -> Void) { + fileprivate init(setupImpl: @escaping (Bool) -> Void, activateImpl: @escaping (ManagedAudioSessionControlActivate) -> Void, setOutputModeImpl: @escaping (AudioSessionOutputMode) -> Void, setupAndActivateImpl: @escaping (Bool, ManagedAudioSessionControlActivate) -> Void, setTypeImpl: @escaping (ManagedAudioSessionType, @escaping () -> Void) -> Void) { self.setupImpl = setupImpl self.activateImpl = activateImpl self.setOutputModeImpl = setOutputModeImpl self.setupAndActivateImpl = setupAndActivateImpl + self.setTypeImpl = setTypeImpl } public func setup(synchronous: Bool = false) { @@ -184,6 +186,10 @@ public class ManagedAudioSessionControl { public func setOutputMode(_ mode: AudioSessionOutputMode) { self.setOutputModeImpl(mode) } + + public func setType(_ audioSessionType: ManagedAudioSessionType, completion: @escaping () -> Void) { + self.setTypeImpl(audioSessionType, completion) + } } public final class ManagedAudioSession: NSObject { @@ -548,6 +554,24 @@ public final class ManagedAudioSession: NSObject { queue.async(f) } } + }, setTypeImpl: { [weak self] audioSessionType, completion in + queue.async { + if let strongSelf = self { + for holder in strongSelf.holders { + if holder.id == id { + if holder.audioSessionType != audioSessionType { + holder.audioSessionType = audioSessionType + } + + if holder.active { + strongSelf.updateAudioSessionType(audioSessionType) + } + } + } + } + + completion() + } }), activate: { [weak self] state in manualActivate(state) queue.async { @@ -801,7 +825,11 @@ public final class ManagedAudioSession: NSObject { switch type { case .play(mixWithOthers: true), .ambient: - try AVAudioSession.sharedInstance().setActive(false) + do { + try AVAudioSession.sharedInstance().setActive(false) + } catch let error { + managedAudioSessionLog("ManagedAudioSession setActive error \(error)") + } default: break } @@ -1004,6 +1032,12 @@ public final class ManagedAudioSession: NSObject { } } + private func updateAudioSessionType(_ audioSessionType: ManagedAudioSessionType) { + if let (_, outputMode) = self.currentTypeAndOutputMode { + self.setup(type: audioSessionType, outputMode: outputMode, activateNow: true) + } + } + private func updateOutputMode(_ outputMode: AudioSessionOutputMode) { if let (type, _) = self.currentTypeAndOutputMode { self.setup(type: type, outputMode: outputMode, activateNow: true) diff --git a/submodules/TelegramCore/Sources/Account/Account.swift b/submodules/TelegramCore/Sources/Account/Account.swift index 284a267ca9..8bb8f2e23f 100644 --- a/submodules/TelegramCore/Sources/Account/Account.swift +++ b/submodules/TelegramCore/Sources/Account/Account.swift @@ -1158,6 +1158,7 @@ public class Account { self.managedOperationsDisposable.add(managedAutoexpireStoryOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedPeerTimestampAttributeOperations(network: self.network, postbox: self.postbox).start()) self.managedOperationsDisposable.add(managedSynchronizeViewStoriesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) + self.managedOperationsDisposable.add(managedSynchronizePeerStoriesOperations(postbox: self.postbox, network: self.network, stateManager: self.stateManager).start()) self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start()) let extractedExpr: [Signal] = [ diff --git a/submodules/TelegramCore/Sources/Account/AccountManager.swift b/submodules/TelegramCore/Sources/Account/AccountManager.swift index 1e47a0d370..c94b3ec025 100644 --- a/submodules/TelegramCore/Sources/Account/AccountManager.swift +++ b/submodules/TelegramCore/Sources/Account/AccountManager.swift @@ -197,6 +197,7 @@ private var declaredEncodables: Void = { declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) }) declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) }) declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) }) + declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) }) return }() diff --git a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift index e10e1dfb5f..63a2ecd68b 100644 --- a/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift +++ b/submodules/TelegramCore/Sources/State/AccountStateManagementUtils.swift @@ -4489,6 +4489,11 @@ func replayFinalState( updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp, isCloseFriends: storedItem.isCloseFriends)) } } + if case .item = storedItem { + if let codedEntry = CodableEntry(storedItem) { + transaction.setStory(id: StoryId(peerId: peerId, id: storedItem.id), value: codedEntry) + } + } } else { if case let .storyItemDeleted(id) = story { if let index = updatedPeerEntries.firstIndex(where: { $0.id == id }) { @@ -4981,5 +4986,18 @@ func replayFinalState( requestChatListFiltersSync(transaction: transaction) } + for update in storyUpdates { + switch update { + case let .added(peerId, _): + if shouldKeepUserStoriesInFeed(peerId: peerId, isContact: transaction.isPeerContact(peerId: peerId)) { + if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) && !transaction.storySubscriptionsContains(key: .filtered, peerId: peerId) { + _internal_addSynchronizePeerStoriesOperation(peerId: peerId, transaction: transaction) + } + } + default: + break + } + } + return AccountReplayedFinalState(state: finalState, addedIncomingMessageIds: addedIncomingMessageIds, addedReactionEvents: addedReactionEvents, wasScheduledMessageIds: wasScheduledMessageIds, addedSecretMessageIds: addedSecretMessageIds, deletedMessageIds: deletedMessageIds, updatedTypingActivities: updatedTypingActivities, updatedWebpages: updatedWebpages, updatedCalls: updatedCalls, addedCallSignalingData: addedCallSignalingData, updatedGroupCallParticipants: updatedGroupCallParticipants, storyUpdates: storyUpdates, updatedPeersNearby: updatedPeersNearby, isContactUpdates: isContactUpdates, delayNotificatonsUntil: delayNotificatonsUntil, updatedIncomingThreadReadStates: updatedIncomingThreadReadStates, updatedOutgoingThreadReadStates: updatedOutgoingThreadReadStates, updateConfig: updateConfig, isPremiumUpdated: isPremiumUpdated) } diff --git a/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift b/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift new file mode 100644 index 0000000000..7da8510625 --- /dev/null +++ b/submodules/TelegramCore/Sources/State/ManagedSynchronizePeerStoriesOperations.swift @@ -0,0 +1,121 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +private final class ManagedSynchronizePeerStoriesOperationsHelper { + var operationDisposables: [PeerId: Disposable] = [:] + + func update(_ entries: [PeerMergedOperationLogEntry]) -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) { + var disposeOperations: [Disposable] = [] + var beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)] = [] + + var validPeerIds: [PeerId] = [] + for entry in entries { + guard let _ = entry.contents as? SynchronizePeerStoriesOperation else { + continue + } + validPeerIds.append(entry.peerId) + var replace = true + if let _ = self.operationDisposables[entry.peerId] { + } else { + replace = true + } + if replace { + let disposable = MetaDisposable() + self.operationDisposables[entry.peerId] = disposable + beginOperations.append((entry, disposable)) + } + } + + var removedPeerIds: [PeerId] = [] + for (peerId, info) in self.operationDisposables { + if !validPeerIds.contains(peerId) { + removedPeerIds.append(peerId) + disposeOperations.append(info) + } + } + for peerId in removedPeerIds { + self.operationDisposables.removeValue(forKey: peerId) + } + + return (disposeOperations, beginOperations) + } + + func reset() -> [Disposable] { + let disposables = Array(self.operationDisposables.values) + self.operationDisposables.removeAll() + return disposables + } +} + +private func withTakenOperation(postbox: Postbox, peerId: PeerId, tagLocalIndex: Int32, _ f: @escaping (Transaction, PeerMergedOperationLogEntry?) -> Signal) -> Signal { + return postbox.transaction { transaction -> Signal in + var result: PeerMergedOperationLogEntry? + transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: tagLocalIndex, { entry in + if let entry = entry, let _ = entry.mergedIndex, entry.contents is SynchronizePeerStoriesOperation { + result = entry.mergedEntry! + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } else { + return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) + } + }) + + return f(transaction, result) + } |> switchToLatest +} + +func managedSynchronizePeerStoriesOperations(postbox: Postbox, network: Network, stateManager: AccountStateManager) -> Signal { + return Signal { _ in + let helper = Atomic(value: ManagedSynchronizePeerStoriesOperationsHelper()) + + let disposable = postbox.mergedOperationLogView(tag: OperationLogTags.SynchronizePeerStories, limit: 10).start(next: { view in + let (disposeOperations, beginOperations) = helper.with { helper -> (disposeOperations: [Disposable], beginOperations: [(PeerMergedOperationLogEntry, MetaDisposable)]) in + return helper.update(view.entries) + } + + for disposable in disposeOperations { + disposable.dispose() + } + + for (entry, disposable) in beginOperations { + let signal = withTakenOperation(postbox: postbox, peerId: entry.peerId, tagLocalIndex: entry.tagLocalIndex, { transaction, entry -> Signal in + if let entry = entry { + if let operation = entry.contents as? SynchronizePeerStoriesOperation { + if let peer = transaction.getPeer(entry.peerId) { + return pushStoriesAreSeen(postbox: postbox, network: network, stateManager: stateManager, peer: peer, operation: operation) + } else { + return .complete() + } + } else { + assertionFailure() + } + } + return .complete() + }) + |> then(postbox.transaction { transaction -> Void in + let _ = transaction.operationLogRemoveEntry(peerId: entry.peerId, tag: OperationLogTags.SynchronizePeerStories, tagLocalIndex: entry.tagLocalIndex) + }) + + disposable.set(signal.start()) + } + }) + + return ActionDisposable { + let disposables = helper.with { helper -> [Disposable] in + return helper.reset() + } + for disposable in disposables { + disposable.dispose() + } + disposable.dispose() + } + } +} + +private func pushStoriesAreSeen(postbox: Postbox, network: Network, stateManager: AccountStateManager, peer: Peer, operation: SynchronizePeerStoriesOperation) -> Signal { + return _internal_pollPeerStories(postbox: postbox, network: network, accountPeerId: stateManager.accountPeerId, peerId: peer.id, peerReference: PeerReference(peer)) + |> map { _ -> Void in + } +} diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift index 353b664c67..d2ec5c083d 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_Namespaces.swift @@ -186,6 +186,7 @@ public struct OperationLogTags { public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22) public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23) public static let SynchronizeViewStories = PeerOperationLogTag(value: 24) + public static let SynchronizePeerStories = PeerOperationLogTag(value: 25) } public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable { diff --git a/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift b/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift new file mode 100644 index 0000000000..fa3713009a --- /dev/null +++ b/submodules/TelegramCore/Sources/SyncCore/SynchronizePeerStoriesOperation.swift @@ -0,0 +1,32 @@ +import Foundation +import Postbox + +public final class SynchronizePeerStoriesOperation: PostboxCoding { + public init() { + } + + public init(decoder: PostboxDecoder) { + } + + public func encode(_ encoder: PostboxEncoder) { + } +} + +func _internal_addSynchronizePeerStoriesOperation(peerId: PeerId, transaction: Transaction) { + let tag: PeerOperationLogTag = OperationLogTags.SynchronizePeerStories + + var topOperation: (SynchronizePeerStoriesOperation, Int32)? + transaction.operationLogEnumerateEntries(peerId: peerId, tag: tag, { entry in + if let operation = entry.contents as? SynchronizePeerStoriesOperation { + topOperation = (operation, entry.tagLocalIndex) + } + return false + }) + var replace = false + if topOperation == nil { + replace = true + } + if replace { + transaction.operationLogAddEntry(peerId: peerId, tag: tag, tagLocalIndex: .automatic, tagMergedIndex: .automatic, contents: SynchronizePeerStoriesOperation()) + } +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift index 14d7948a78..b009f95ebf 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Contacts/ContactManagement.swift @@ -108,6 +108,10 @@ func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId) account.stateManager.addUpdates(updates) } return account.postbox.transaction { transaction -> Void in + if let user = peer as? TelegramUser { + _internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false) + } + var peerIds = transaction.getContactPeerIds() if peerIds.contains(peerId) { peerIds.remove(peerId) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 23f72f6e1c..13c7c2fcbd 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -724,7 +724,7 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran privacyRules = [.inputPrivacyValueAllowCloseFriends] case .nobody: if privacy.additionallyIncludePeers.isEmpty { - privacyRules = [.inputPrivacyValueDisallowAll] + privacyRules = [.inputPrivacyValueAllowUsers(users: [.inputUserSelf])] } else { privacyRules = [] } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 931a126e7b..cb5ae41885 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -790,6 +790,44 @@ public final class PeerStoryListContext { finalUpdatedState = updatedState } } + } else { + if case let .item(item) = item { + if let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items[index] = EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited + ) + finalUpdatedState = updatedState + } else { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.remove(at: index) + updatedState.totalCount = max(0, updatedState.totalCount - 1) + finalUpdatedState = updatedState + } + } } } else { if !self.isArchived { @@ -830,6 +868,42 @@ public final class PeerStoryListContext { } } } + } else { + if case let .item(item) = item { + if let media = item.media { + var updatedState = finalUpdatedState ?? self.stateValue + updatedState.items.append(EngineStoryItem( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: EngineMedia(media), + text: item.text, + entities: item.entities, + views: item.views.flatMap { views in + return EngineStoryItem.Views( + seenCount: views.seenCount, + seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in + return peers[id].flatMap(EnginePeer.init) + } + ) + }, + privacy: item.privacy.flatMap(EngineStoryPrivacy.init), + isPinned: item.isPinned, + isExpired: item.isExpired, + isPublic: item.isPublic, + isPending: false, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited + )) + updatedState.items.sort(by: { lhs, rhs in + return lhs.timestamp > rhs.timestamp + }) + finalUpdatedState = updatedState + } + } } } } @@ -1187,9 +1261,9 @@ public final class PeerExpiringStoryListContext { } } -public func _internal_pollPeerStories(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId) -> Signal { +public func _internal_pollPeerStories(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, peerReference: PeerReference? = nil) -> Signal { return postbox.transaction { transaction -> Api.InputUser? in - return transaction.getPeer(peerId).flatMap(apiInputUser) + return transaction.getPeer(peerId).flatMap(apiInputUser) ?? peerReference?.inputUser } |> mapToSignal { inputUser -> Signal in guard let inputUser = inputUser else { @@ -1236,7 +1310,7 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries) - if !updatedPeerEntries.isEmpty { + if !updatedPeerEntries.isEmpty, shouldKeepUserStoriesInFeed(peerId: peerId, isContact: transaction.isPeerContact(peerId: peerId)) { if let user = transaction.getPeer(peerId) as? TelegramUser, let storiesHidden = user.storiesHidden { if storiesHidden { if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) { diff --git a/submodules/TelegramCore/Sources/UpdatePeers.swift b/submodules/TelegramCore/Sources/UpdatePeers.swift index a2dd3ffa05..1715df1ba8 100644 --- a/submodules/TelegramCore/Sources/UpdatePeers.swift +++ b/submodules/TelegramCore/Sources/UpdatePeers.swift @@ -38,6 +38,13 @@ func minTimestampForPeerInclusion(_ peer: Peer) -> Int32? { } } +func shouldKeepUserStoriesInFeed(peerId: PeerId, isContact: Bool) -> Bool { + if peerId.namespace == Namespaces.Peer.CloudUser && (peerId.id._internalGetInt64Value() == 777000 || peerId.id._internalGetInt64Value() == 333000) { + return true + } + return isContact +} + func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: AccumulatedPeers) { var parsedPeers: [Peer] = [] for (_, user) in peers.users { @@ -47,11 +54,17 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId): let isMin = (flags & (1 << 20)) != 0 let storiesUnavailable = (flags2 & (1 << 4)) != 0 + if let storiesMaxId = storiesMaxId { transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId) } else if !isMin && storiesUnavailable { transaction.clearStoryItemsInexactMaxId(peerId: user.peerId) } + + if !isMin { + let isContact = (flags & (1 << 11)) != 0 + _internal_updatePeerIsContact(transaction: transaction, user: telegramUser, isContact: isContact) + } case .userEmpty: break } @@ -65,6 +78,54 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peers.users) } +func _internal_updatePeerIsContact(transaction: Transaction, user: TelegramUser, isContact: Bool) { + let previousValue = shouldKeepUserStoriesInFeed(peerId: user.id, isContact: transaction.isPeerContact(peerId: user.id)) + let updatedValue = shouldKeepUserStoriesInFeed(peerId: user.id, isContact: isContact) + + if previousValue != updatedValue, let storiesHidden = user.storiesHidden { + if updatedValue { + if storiesHidden { + if transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + if !transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + if !peerIds.contains(user.id) { + peerIds.append(user.id) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + } + } else { + if transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + if !transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + if !peerIds.contains(user.id) { + peerIds.append(user.id) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + } + } + } else { + if transaction.storySubscriptionsContains(key: .filtered, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } + if transaction.storySubscriptionsContains(key: .hidden, peerId: user.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == user.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } + } + } +} + public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: (Peer?, Peer) -> Peer?) { transaction.updatePeersInternal(peers, update: { previous, updated in let peerId = updated.id @@ -79,32 +140,34 @@ public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: ( switch peerId.namespace { case Namespaces.Peer.CloudUser: - if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser, let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden { - if storiesHidden { - if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) - peerIds.removeAll(where: { $0 == updated.id }) - transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) - - if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) - if !peerIds.contains(updated.id) { - peerIds.append(updated.id) - transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser { + if let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden { + if storiesHidden { + if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + peerIds.removeAll(where: { $0 == updated.id }) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + + if !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + if !peerIds.contains(updated.id) { + peerIds.append(updated.id) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + } } } - } - } else { - if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) - peerIds.removeAll(where: { $0 == updated.id }) - transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) - - if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { - var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) - if !peerIds.contains(updated.id) { - peerIds.append(updated.id) - transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } else { + if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden) + peerIds.removeAll(where: { $0 == updated.id }) + transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds) + + if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) { + var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered) + if !peerIds.contains(updated.id) { + peerIds.append(updated.id) + transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds) + } } } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 5aae15186d..bc21449495 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -270,6 +270,13 @@ final class PeerInfoStoryGridScreenComponent: Component { }) } + func scrollToTop() { + guard let paneNode = self.paneNode else { + return + } + let _ = paneNode.scrollToTop() + } + func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.component = component self.state = state @@ -500,6 +507,13 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { self.navigationItem.titleView = self.titleView self.updateTitle() + + self.scrollToTop = { [weak self] in + guard let self, let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { + return + } + componentView.scrollToTop() + } } required public init(coder aDecoder: NSCoder) { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 1c317bad07..b681623fab 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -329,10 +329,14 @@ private final class GenericItemLayer: CALayer, ItemLayer { return !self.hasContents } - func update(size: CGSize) { - /*if let durationLayer = self.durationLayer { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } + + if let binding = binding as? SparseItemGridBindingImpl, let item = item as? VisualMediaItem, let previousItem = self.item, previousItem.story.media.id != item.story.media.id { + binding.bindLayers(items: [item], layers: [displayItem], size: size, insets: insets, synchronous: .none) + } } } @@ -469,10 +473,10 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL return !self.hasContents } - func update(size: CGSize) { - /*if let durationLayer = self.durationLayer { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { + if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) - }*/ + } } } @@ -1229,7 +1233,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } self.itemGridBinding.itemInteraction = self._itemInteraction - self.contextGestureContainerNode.isGestureEnabled = true + self.contextGestureContainerNode.isGestureEnabled = false self.contextGestureContainerNode.addSubnode(self.itemGrid) self.addSubnode(self.contextGestureContainerNode) @@ -1631,6 +1635,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false) } self.update(size: size, topInset: topInset, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, isScrollingLockedAtTop: isScrollingLockedAtTop, expandProgress: expandProgress, presentationData: presentationData, synchronous: false, transition: .immediate) + self.updateSelectedItems(animated: false) if let gridSnapshot = gridSnapshot { self.view.addSubview(gridSnapshot) gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in @@ -1733,16 +1738,16 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.itemGrid.addToTransitionSurface(view: view) } - private var gridSelectionGesture: MediaPickerGridSelectionGesture? + private var gridSelectionGesture: MediaPickerGridSelectionGesture? override public func didLoad() { super.didLoad() - let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) + /*let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:))) selectionRecognizer.shouldBegin = { return true } - self.view.addGestureRecognizer(selectionRecognizer) + self.view.addGestureRecognizer(selectionRecognizer)*/ } private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])? @@ -1807,17 +1812,18 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - let location = gestureRecognizer.location(in: gestureRecognizer.view) + /*let location = gestureRecognizer.location(in: gestureRecognizer.view) if location.x < 44.0 { return false - } + }*/ return true } public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer { - otherGestureRecognizer.isEnabled = false - otherGestureRecognizer.isEnabled = true + let _ = otherGestureRecognizer + //otherGestureRecognizer.isEnabled = false + //otherGestureRecognizer.isEnabled = true return true } else { return false @@ -1841,27 +1847,34 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr itemLayer.updateSelection(theme: self.itemGridBinding.checkNodeTheme, isSelected: self.itemInteraction.selectedIds?.contains(item.story.id), animated: animated) } - /*let isSelecting = self.chatControllerInteraction.selectionState != nil + let isSelecting = self._itemInteraction?.selectedIds != nil self.itemGrid.pinchEnabled = !isSelecting + var enableDismissGesture = true + if let items = self.items, items.items.isEmpty { + } else if isSelecting { + enableDismissGesture = false + } + self.view.disablesInteractiveTransitionGestureRecognizer = enableDismissGesture + if isSelecting { if self.gridSelectionGesture == nil { - let selectionGesture = MediaPickerGridSelectionGesture() + let selectionGesture = MediaPickerGridSelectionGesture() selectionGesture.delegate = self selectionGesture.sideInset = 44.0 selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in self?.itemGrid.isScrollEnabled = isEnabled } selectionGesture.itemAt = { [weak self] point in - if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let messageId = itemLayer.item?.message.id { - return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false) + if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let storyId = itemLayer.item?.story.id { + return (storyId, strongSelf._itemInteraction?.selectedIds?.contains(storyId) ?? false) } else { return nil } } - selectionGesture.updateSelection = { [weak self] messageId, selected in + selectionGesture.updateSelection = { [weak self] storyId, selected in if let strongSelf = self { - strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected) + strongSelf._itemInteraction?.toggleSelection(storyId, selected) } } self.itemGrid.view.addGestureRecognizer(selectionGesture) @@ -1870,7 +1883,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } else if let gridSelectionGesture = self.gridSelectionGesture { self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture) self.gridSelectionGesture = nil - }*/ + } } private func updateHiddenItems() { diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index 7b09f0d194..be2213e218 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -473,7 +473,7 @@ private final class GenericItemLayer: CALayer, ItemLayer { return !self.hasContents } - func update(size: CGSize) { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { /*if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) }*/ @@ -613,7 +613,7 @@ private final class CaptureProtectedItemLayer: AVSampleBufferDisplayLayer, ItemL return !self.hasContents } - func update(size: CGSize) { + func update(size: CGSize, insets: UIEdgeInsets, displayItem: SparseItemGridDisplayItem, binding: SparseItemGridBinding, item: SparseItemGrid.Item?) { /*if let durationLayer = self.durationLayer { durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize()) }*/ diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift index 60e966250b..edb0754620 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/OpenStories.swift @@ -171,7 +171,7 @@ public extension StoryContainerScreen { |> take(1) |> mapToSignal { state -> Signal in if let slice = state.slice { - #if DEBUG && true + #if DEBUG && false if "".isEmpty { return .single(state) |> delay(4.0, queue: .mainQueue()) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 9dcc82eda6..241d8be961 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -914,7 +914,9 @@ public final class StoryContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + } } } @@ -1094,7 +1096,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { public func markAsSeen(id: StoryId) { if self.readGlobally { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: false).start() + } } } } @@ -1397,7 +1401,9 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { } public func markAsSeen(id: StoryId) { - let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start() + if !self.context.sharedContext.immediateExperimentalUISettings.skipReadHistory { + let _ = self.context.engine.messages.markStoryAsSeen(peerId: id.peerId, id: id.id, asPinned: true).start() + } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 714d4dfc4c..f60b7dbf20 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -153,7 +153,8 @@ final class StoryItemContentComponent: Component { imageReference: nil, streamVideo: .story, loopVideo: true, - enableSound: component.audioMode != .off, + enableSound: true, + soundMuted: component.audioMode == .off, beginWithAmbientSound: component.audioMode == .ambient, mixWithOthers: true, useLargeThumbnail: false, @@ -255,7 +256,7 @@ final class StoryItemContentComponent: Component { override func leaveAmbientMode() { if let videoNode = self.videoNode { self.ignoreBufferingTimestamp = CFAbsoluteTimeGetCurrent() - videoNode.setSoundEnabled(true) + videoNode.setSoundMuted(soundMuted: false) videoNode.continueWithOverridingAmbientMode(isAmbient: false) } } @@ -266,7 +267,7 @@ final class StoryItemContentComponent: Component { if ambient { videoNode.continueWithOverridingAmbientMode(isAmbient: true) } else { - videoNode.setSoundEnabled(false) + videoNode.setSoundMuted(soundMuted: true) } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift index ef6423d674..8e54e4521d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemImageView.swift @@ -25,8 +25,6 @@ final class StoryItemImageView: UIView { private(set) var isContentLoaded: Bool = false var didLoadContents: (() -> Void)? - private var isCaptureProtected: Bool = false - override init(frame: CGRect) { self.contentView = UIImageView() self.contentView.contentMode = .scaleAspectFill @@ -44,8 +42,8 @@ final class StoryItemImageView: UIView { self.disposable?.dispose() } - private func updateImage(image: UIImage) { - if self.isCaptureProtected { + private func updateImage(image: UIImage, isCaptureProtected: Bool) { + if isCaptureProtected { let captureProtectedContentLayer: CaptureProtectedContentLayer if let current = self.captureProtectedContentLayer { captureProtectedContentLayer = current @@ -71,8 +69,6 @@ final class StoryItemImageView: UIView { } func update(context: AccountContext, strings: PresentationStrings, peer: EnginePeer, storyId: Int32, media: EngineMedia, size: CGSize, isCaptureProtected: Bool, attemptSynchronous: Bool, transition: Transition) { - self.isCaptureProtected = isCaptureProtected - self.backgroundColor = isCaptureProtected ? UIColor(rgb: 0x181818) : nil var dimensions: CGSize? @@ -90,14 +86,28 @@ final class StoryItemImageView: UIView { dimensions = representation.dimensions.cgSize if isMediaUpdated { + if isCaptureProtected { + if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { + if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { + self.updateImage(image: image, isCaptureProtected: false) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + guard let self else { + return + } + self.contentView.image = nil + }) + } + } + } + if attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } else { if let image = UIImage(contentsOfFile: path)?.precomposed() { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } self.isContentLoaded = true @@ -105,7 +115,7 @@ final class StoryItemImageView: UIView { } else { if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } @@ -137,7 +147,7 @@ final class StoryItemImageView: UIView { return } if let image { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) self.isContentLoaded = true self.didLoadContents?() } @@ -149,16 +159,30 @@ final class StoryItemImageView: UIView { dimensions = file.dimensions?.cgSize if isMediaUpdated { + if isCaptureProtected { + if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { + if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { + self.updateImage(image: image, isCaptureProtected: false) + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { [weak self] in + guard let self else { + return + } + self.contentView.image = nil + }) + } + } + } + let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation()) if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) { if #available(iOS 15.0, *) { if let image = UIImage(contentsOfFile: cachedPath)?.preparingForDisplay() { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } else { if let image = UIImage(contentsOfFile: cachedPath)?.precomposed() { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } self.isContentLoaded = true @@ -166,7 +190,7 @@ final class StoryItemImageView: UIView { } else { if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) { if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) } } @@ -195,7 +219,7 @@ final class StoryItemImageView: UIView { return } if let image { - self.updateImage(image: image) + self.updateImage(image: image, isCaptureProtected: isCaptureProtected) self.isContentLoaded = true self.didLoadContents?() } @@ -217,7 +241,7 @@ final class StoryItemImageView: UIView { } } - if self.isCaptureProtected { + if isCaptureProtected { let captureProtectedInfo: ComponentView var captureProtectedInfoTransition = transition if let current = self.captureProtectedInfo { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e369c6463d..852f14994c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -271,6 +271,7 @@ public final class StoryItemSetContainerComponent: Component { final class VisibleItem { let externalState = StoryContentItem.ExternalState() let contentContainerView: UIView + let contentTintLayer = SimpleLayer() let view = ComponentView() var currentProgress: Double = 0.0 var isBuffering: Bool = false @@ -282,6 +283,8 @@ public final class StoryItemSetContainerComponent: Component { if #available(iOS 13.0, *) { self.contentContainerView.layer.cornerCurve = .continuous } + + self.contentTintLayer.backgroundColor = UIColor(white: 0.0, alpha: 1.0).cgColor } } @@ -952,6 +955,9 @@ public final class StoryItemSetContainerComponent: Component { if self.sendMessageContext.shareController != nil { return .pause } + if self.sendMessageContext.statusController != nil { + return .pause + } if let navigationController = component.controller()?.navigationController as? NavigationController { let topViewController = navigationController.topViewController if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) { @@ -1119,6 +1125,7 @@ public final class StoryItemSetContainerComponent: Component { if let view = visibleItem.view.view { if visibleItem.contentContainerView.superview == nil { self.itemsContainerView.addSubview(visibleItem.contentContainerView) + self.itemsContainerView.layer.addSublayer(visibleItem.contentTintLayer) visibleItem.contentContainerView.addSubview(view) } @@ -1137,6 +1144,9 @@ public final class StoryItemSetContainerComponent: Component { }) itemTransition.setBounds(view: visibleItem.contentContainerView, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) + itemTransition.setPosition(layer: visibleItem.contentTintLayer, position: CGPoint(x: itemPositionX, y: itemLayout.contentFrame.center.y)) + itemTransition.setBounds(layer: visibleItem.contentTintLayer, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) + var transform = CATransform3DMakeScale(itemScale, itemScale, 1.0) if let pinchState = component.pinchState { let pinchOffset = CGPoint( @@ -1154,6 +1164,8 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setTransform(view: visibleItem.contentContainerView, transform: transform) itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale)) + itemTransition.setTransform(layer: visibleItem.contentTintLayer, transform: transform) + let countedFractionDistanceToCenter: CGFloat = max(0.0, min(1.0, unboundFractionDistanceToCenter / 3.0)) var itemAlpha: CGFloat = 1.0 * (1.0 - countedFractionDistanceToCenter) + 0.0 * countedFractionDistanceToCenter itemAlpha = max(0.0, min(1.0, itemAlpha)) @@ -1161,7 +1173,7 @@ public final class StoryItemSetContainerComponent: Component { let collapsedAlpha = itemAlpha * itemLayout.contentScaleFraction + 0.0 * (1.0 - itemLayout.contentScaleFraction) itemAlpha = (1.0 - fractionDistanceToCenter) * itemAlpha + fractionDistanceToCenter * collapsedAlpha - itemTransition.setAlpha(view: visibleItem.contentContainerView, alpha: itemAlpha) + itemTransition.setAlpha(layer: visibleItem.contentTintLayer, alpha: 1.0 - itemAlpha) var itemProgressMode = self.itemProgressMode() if index != centralIndex { @@ -1182,6 +1194,7 @@ public final class StoryItemSetContainerComponent: Component { if !validIds.contains(id) { removeIds.append(id) visibleItem.contentContainerView.removeFromSuperview() + visibleItem.contentTintLayer.removeFromSuperlayer() } } for id in removeIds { @@ -1935,7 +1948,7 @@ public final class StoryItemSetContainerComponent: Component { wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed, timeoutValue: nil, timeoutSelected: false, - displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular, + displayGradient: false, bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset, hideKeyboard: self.sendMessageContext.currentInputMode == .media, forceIsEditing: self.sendMessageContext.currentInputMode == .media, @@ -2506,7 +2519,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: .plain(text: tooltipText), style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + text: .markdown(text: tooltipText), style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in return .dismiss(consume: true) } ) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 794a1f806f..3cbf85aa86 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -57,6 +57,7 @@ final class StoryItemSetContainerSendMessage { weak var shareController: ShareController? weak var tooltipScreen: ViewController? weak var actionSheet: ViewController? + weak var statusController: ViewController? var isViewingAttachedStickers = false var currentInputMode: InputMode = .text @@ -363,6 +364,8 @@ final class StoryItemSetContainerSendMessage { controller.present(tooltipScreen, in: .current) self.tooltipScreen = tooltipScreen view.updateIsProgressPaused() + + HapticFeedback().success() } func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) { @@ -2471,14 +2474,21 @@ final class StoryItemSetContainerSendMessage { var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak self, weak view, weak parentController] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) + + self?.statusController = controller + view?.updateIsProgressPaused() + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } @@ -2545,14 +2555,21 @@ final class StoryItemSetContainerSendMessage { } var cancelImpl: (() -> Void)? let presentationData = component.context.sharedContext.currentPresentationData.with { $0 } - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: { cancelImpl?() })) parentController?.present(controller, in: .window(.root)) + + self?.statusController = controller + view?.updateIsProgressPaused() + return ActionDisposable { [weak controller] in Queue.mainQueue().async() { controller?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } @@ -2737,12 +2754,19 @@ final class StoryItemSetContainerSendMessage { } let context = component.context let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - let progressSignal = Signal { [weak parentController] subscriber in + let progressSignal = Signal { [weak parentController, weak self, weak view] subscriber in let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil)) parentController?.present(progressController, in: .window(.root), with: nil) + + self?.statusController = progressController + view?.updateIsProgressPaused() + return ActionDisposable { [weak progressController] in Queue.mainQueue().async() { progressController?.dismiss() + + self?.statusController = nil + view?.updateIsProgressPaused() } } } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift index 73be5d761f..f9fe30d554 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListComponent.swift @@ -356,6 +356,8 @@ public final class StoryPeerListComponent: Component { public private(set) var overscrollSelectedId: EnginePeer.Id? public private(set) var overscrollHiddenChatItemsAllowed: Bool = false + private var anchorForTooltipRect: CGRect? + private var sharedBlurEffect: NSObject? public override init(frame: CGRect) { @@ -482,7 +484,11 @@ public final class StoryPeerListComponent: Component { } public func anchorForTooltip() -> (UIView, CGRect)? { - return (self.collapsedButton, self.collapsedButton.bounds) + if let anchorForTooltipRect = self.anchorForTooltipRect { + return (self, anchorForTooltipRect) + } else { + return nil + } } public func titleFrame() -> CGRect { @@ -631,6 +637,7 @@ public final class StoryPeerListComponent: Component { var minFraction: CGFloat var maxFraction: CGFloat var sideAlphaFraction: CGFloat + var expandEffectFraction: CGFloat var titleWidth: CGFloat var activityFraction: CGFloat } @@ -668,6 +675,19 @@ public final class StoryPeerListComponent: Component { let timestamp = CACurrentMediaTime() + let calculateOverscrollEffectFraction: (CGFloat, CGFloat) -> CGFloat = { maxFraction, bounceFraction in + var expandEffectFraction: CGFloat = max(0.0, min(1.0, maxFraction)) + expandEffectFraction = pow(expandEffectFraction, 1.0) + + let overscrollEffectFraction = max(0.0, maxFraction - 1.0) + expandEffectFraction += overscrollEffectFraction * 0.12 + + let reverseBounceFraction = 1.0 - pow(1.0 - bounceFraction, 2.4) + expandEffectFraction += reverseBounceFraction * 0.09 * maxFraction + + return expandEffectFraction + } + let collapsedState: CollapseState let expandBoundsFraction: CGFloat if let animationState = self.animationState { @@ -703,16 +723,6 @@ public final class StoryPeerListComponent: Component { let animatedTitleWidth = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromTitleWidth, toFraction: realTitleContentWidth) let animatedActivityFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromActivityFraction, toFraction: targetActivityFraction) - collapsedState = CollapseState( - globalFraction: animatedGlobalFraction, - scaleFraction: animatedScaleFraction, - minFraction: animatedMinFraction, - maxFraction: animatedMaxFraction, - sideAlphaFraction: animatedSideAlphaFraction, - titleWidth: animatedTitleWidth, - activityFraction: animatedActivityFraction - ) - var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration) rawProgress = max(0.0, min(1.0, rawProgress)) @@ -724,6 +734,17 @@ public final class StoryPeerListComponent: Component { } else { expandBoundsFraction = 0.0 } + + collapsedState = CollapseState( + globalFraction: animatedGlobalFraction, + scaleFraction: animatedScaleFraction, + minFraction: animatedMinFraction, + maxFraction: animatedMaxFraction, + sideAlphaFraction: animatedSideAlphaFraction, + expandEffectFraction: calculateOverscrollEffectFraction(animatedMaxFraction, expandBoundsFraction), + titleWidth: animatedTitleWidth, + activityFraction: animatedActivityFraction + ) } else { collapsedState = CollapseState( globalFraction: targetFraction, @@ -731,6 +752,7 @@ public final class StoryPeerListComponent: Component { minFraction: targetMinFraction, maxFraction: targetMaxFraction, sideAlphaFraction: targetSideAlphaFraction, + expandEffectFraction: calculateOverscrollEffectFraction(targetMaxFraction, 0.0), titleWidth: realTitleContentWidth, activityFraction: targetActivityFraction ) @@ -817,7 +839,7 @@ public final class StoryPeerListComponent: Component { } //print("overscrollStage2: \(overscrollStage2)") - if let overscrollFocusIndex, overscrollStage2 >= 1.25 { + if let overscrollFocusIndex, overscrollStage2 >= 1.19 { self.overscrollSelectedId = self.sortedItems[overscrollFocusIndex].peer.id } else { self.overscrollSelectedId = nil @@ -1031,6 +1053,7 @@ public final class StoryPeerListComponent: Component { scale: itemScale, fullWidth: expandedItemWidth, expandedAlphaFraction: collapsedState.sideAlphaFraction, + expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, action: component.peerAction, @@ -1166,6 +1189,7 @@ public final class StoryPeerListComponent: Component { scale: itemScale, fullWidth: expandedItemWidth, expandedAlphaFraction: collapsedState.sideAlphaFraction, + expandEffectFraction: collapsedState.expandEffectFraction, leftNeighborDistance: leftNeighborDistance, rightNeighborDistance: rightNeighborDistance, action: component.peerAction, @@ -1229,6 +1253,7 @@ public final class StoryPeerListComponent: Component { } transition.setFrame(view: self.collapsedButton, frame: CGRect(origin: CGPoint(x: component.minTitleX, y: 6.0 - 59.0), size: CGSize(width: max(0.0, component.maxTitleX - component.minTitleX), height: 44.0))) + self.anchorForTooltipRect = CGRect(origin: CGPoint(x: collapsedContentOrigin, y: -59.0 + 6.0 + 2.0), size: CGSize(width: collapsedContentWidth, height: 44.0)) let defaultCollapsedTitleOffset: CGFloat = 0.0 @@ -1245,6 +1270,8 @@ public final class StoryPeerListComponent: Component { titleContentOffset = titleMinContentOffset.interpolate(to: ((itemLayout.containerSize.width - collapsedState.titleWidth) * 0.5) as CGFloat, amount: min(1.0, collapsedState.maxFraction) * (1.0 - collapsedState.activityFraction)) } + titleContentOffset += -expandBoundsFraction * 4.0 + var titleIndicatorSize: CGSize? if collapsedState.activityFraction != 0.0 { let collapsedItemMinX = collapsedContentOrigin - collapsedItemWidth * 0.5 @@ -1484,6 +1511,11 @@ public final class StoryPeerListComponent: Component { ) } + var allowBounce = !previousComponent.unlocked && component.unlocked + if let animationHint, !animationHint.bounce { + allowBounce = false + } + self.animationState = AnimationState( duration: duration * UIView.animationDurationFactor(), fromIsUnlocked: previousComponent.unlocked, @@ -1491,7 +1523,7 @@ public final class StoryPeerListComponent: Component { fromTitleWidth: self.currentTitleWidth, fromActivityFraction: self.currentActivityFraction, startTime: timestamp, - bounce: animationHint?.bounce ?? true + bounce: allowBounce ) } diff --git a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift index 110968e8aa..c3e38c3e5d 100644 --- a/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryPeerListComponent/Sources/StoryPeerListItemComponent.swift @@ -57,7 +57,7 @@ private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint, return (point1Angle, point2Angle) } -private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool, segmentFraction: CGFloat) -> CGPath { +private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, rightCenter: CGPoint?, radius: CGFloat, totalCount: Int, unseenCount: Int, isSeen: Bool, segmentFraction: CGFloat, rotationFraction: CGFloat) -> CGPath { let leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } let rightAngles = rightCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) } @@ -113,7 +113,7 @@ private func calculateMergingCircleShape(center: CGPoint, leftCenter: CGPoint?, } var startAngle = segmentSpacingAngle * 0.5 - CGFloat.pi * 0.5 + CGFloat(i) * (segmentSpacingAngle + segmentAngle) - startAngle += (1.0 - segmentFraction) * CGFloat.pi * 2.0 * (-0.25) + startAngle += -1.0 * (1.0 - rotationFraction) * CGFloat.pi * 2.0 * 0.25 let endAngle = startAngle + segmentAngle path.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius)) @@ -343,6 +343,7 @@ public final class StoryPeerListItemComponent: Component { public let scale: CGFloat public let fullWidth: CGFloat public let expandedAlphaFraction: CGFloat + public let expandEffectFraction: CGFloat public let leftNeighborDistance: CGPoint? public let rightNeighborDistance: CGPoint? public let action: (EnginePeer) -> Void @@ -361,6 +362,7 @@ public final class StoryPeerListItemComponent: Component { scale: CGFloat, fullWidth: CGFloat, expandedAlphaFraction: CGFloat, + expandEffectFraction: CGFloat, leftNeighborDistance: CGPoint?, rightNeighborDistance: CGPoint?, action: @escaping (EnginePeer) -> Void, @@ -378,6 +380,7 @@ public final class StoryPeerListItemComponent: Component { self.scale = scale self.fullWidth = fullWidth self.expandedAlphaFraction = expandedAlphaFraction + self.expandEffectFraction = expandEffectFraction self.leftNeighborDistance = leftNeighborDistance self.rightNeighborDistance = rightNeighborDistance self.action = action @@ -421,6 +424,9 @@ public final class StoryPeerListItemComponent: Component { if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction { return false } + if lhs.expandEffectFraction != rhs.expandEffectFraction { + return false + } if lhs.leftNeighborDistance != rhs.leftNeighborDistance { return false } @@ -785,8 +791,8 @@ public final class StoryPeerListItemComponent: Component { } Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction)) - Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction)) + Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeSeenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: true, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) + Transition.immediate.setShapeLayerPath(layer: self.indicatorShapeUnseenLayer, path: calculateMergingCircleShape(center: indicatorCenter, leftCenter: mappedLeftCenter, rightCenter: mappedRightCenter, radius: indicatorRadius - indicatorLineUnseenWidth * 0.5, totalCount: component.totalCount, unseenCount: component.unseenCount, isSeen: false, segmentFraction: component.expandedAlphaFraction, rotationFraction: component.expandEffectFraction)) let titleString: String if component.peer.id == component.context.account.peerId { diff --git a/submodules/TelegramUI/Sources/AppDelegate.swift b/submodules/TelegramUI/Sources/AppDelegate.swift index 1398c1d0db..72cb7ec47f 100644 --- a/submodules/TelegramUI/Sources/AppDelegate.swift +++ b/submodules/TelegramUI/Sources/AppDelegate.swift @@ -2392,7 +2392,6 @@ private func extractAccountManagerState(records: AccountRecordsView Void) { let _ = (accountIdFromNotification(response.notification, sharedContext: self.sharedContextPromise.get()) |> deliverOnMainQueue).start(next: { accountId in @@ -2493,11 +2492,11 @@ private func extractAccountManagerState(records: AccountRecordsView deliverOnMainQueue).start(next: { displayNames in - self.registerForNotifications(replyString: presentationData.strings.Notification_Reply, messagePlaceholderString: presentationData.strings.Conversation_InputTextPlaceholder, hiddenContentString: presentationData.strings.Watch_MessageView_Title, hiddenReactionContentString: presentationData.strings.Notification_LockScreenReactionPlaceholder, includeNames: displayNames, authorize: authorize, completion: completion) + self.registerForNotifications(replyString: presentationData.strings.Notification_Reply, messagePlaceholderString: presentationData.strings.Conversation_InputTextPlaceholder, hiddenContentString: presentationData.strings.Watch_MessageView_Title, hiddenReactionContentString: presentationData.strings.Notification_LockScreenReactionPlaceholder, hiddenStoryContentString: presentationData.strings.Notification_LockScreenStoryPlaceholder, includeNames: displayNames, authorize: authorize, completion: completion) }) } - private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, hiddenReactionContentString: String, includeNames: Bool, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) { + private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, hiddenReactionContentString: String, hiddenStoryContentString: String, includeNames: Bool, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) { let notificationCenter = UNUserNotificationCenter.current() Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))") notificationCenter.getNotificationSettings(completionHandler: { settings in @@ -2527,38 +2526,28 @@ private func extractAccountManagerState(records: AccountRecordsView Bool { for media in message.media { if let _ = media as? TelegramMediaAction { return false + } else if let story = media as? TelegramMediaStory { + if story.isMention { + return false + } } } return true diff --git a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift index 25312c768e..51fb13dc1b 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceStateContextMenus.swift @@ -151,6 +151,9 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo } else if let _ = media as? TelegramMediaInvoice { hasUneditableAttributes = true break + } else if let _ = media as? TelegramMediaStory { + hasUneditableAttributes = true + break } } @@ -562,6 +565,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } else if let dice = media as? TelegramMediaDice { diceEmoji = dice.emoji + } else if let story = media as? TelegramMediaStory { + if story.isMention { + isAction = true + } } } } @@ -626,6 +633,8 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState if let story = media as? TelegramMediaStory { if let story = message.associatedStories[story.storyId], story.data.isEmpty { canPin = false + } else if story.isMention { + canPin = false } } } @@ -875,136 +884,6 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState } } } - - if context.sharedContext.immediateExperimentalUISettings.enableReactionOverrides { - for media in message.media { - if let file = media as? TelegramMediaFile, file.isAnimatedSticker { - actions.append(.action(ContextMenuActionItem(text: "Set as Reaction Effect", icon: { _ in - return nil - }, action: { c, _ in - let subItems: Signal = context.engine.stickers.availableReactions() - |> map { reactions -> ContextController.Items in - var subActions: [ContextMenuItem] = [] - - if let reactions = reactions { - for reaction in reactions.reactions { - if !reaction.isEnabled || !reaction.isPremium { - continue - } - - guard case let .builtin(emojiValue) = reaction.value else { - continue - } - - subActions.append(.action(ContextMenuActionItem(text: emojiValue, icon: { _ in - return nil - }, action: { _, f in - let _ = updateExperimentalUISettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in - var settings = settings - - var currentItems: [ExperimentalUISettings.AccountReactionOverrides.Item] - if let value = settings.accountReactionEffectOverrides.first(where: { $0.accountId == context.account.id.int64 }) { - currentItems = value.items - } else { - currentItems = [] - } - - currentItems.removeAll(where: { $0.key == reaction.value }) - currentItems.append(ExperimentalUISettings.AccountReactionOverrides.Item( - key: reaction.value, - messageId: message.id, - mediaId: file.fileId - )) - - settings.accountReactionEffectOverrides.removeAll(where: { $0.accountId == context.account.id.int64 }) - settings.accountReactionEffectOverrides.append(ExperimentalUISettings.AccountReactionOverrides(accountId: context.account.id.int64, items: currentItems)) - - return settings - }).start() - - f(.default) - }))) - } - } - - return ContextController.Items(content: .list(subActions), disablePositionLock: true, tip: nil) - } - - c.pushItems(items: subItems) - }))) - - actions.append(.action(ContextMenuActionItem(text: "Set as Sticker Effect", icon: { _ in - return nil - }, action: { c, _ in - let stickersKey: PostboxViewKey = .orderedItemList(id: Namespaces.OrderedItemList.CloudPremiumStickers) - let subItems: Signal = context.account.postbox.combinedView(keys: [stickersKey]) - |> map { views -> [String] in - if let view = views.views[stickersKey] as? OrderedItemListView, !view.items.isEmpty { - return view.items.compactMap { item -> String? in - guard let mediaItem = item.contents.get(RecentMediaItem.self) else { - return nil - } - let file = mediaItem.media - for attribute in file.attributes { - switch attribute { - case let .Sticker(text, _, _): - return text - default: - break - } - } - return nil - } - } else { - return [] - } - } - |> map { stickerNames -> ContextController.Items in - var subActions: [ContextMenuItem] = [] - - for stickerName in stickerNames { - subActions.append(.action(ContextMenuActionItem(text: stickerName, icon: { _ in - return nil - }, action: { _, f in - let _ = updateExperimentalUISettingsInteractively(accountManager: context.sharedContext.accountManager, { settings in - var settings = settings - - var currentItems: [ExperimentalUISettings.AccountReactionOverrides.Item] - if let value = settings.accountStickerEffectOverrides.first(where: { $0.accountId == context.account.id.int64 }) { - currentItems = value.items - } else { - currentItems = [] - } - - currentItems.removeAll(where: { $0.key == MessageReaction.Reaction.builtin(stickerName) }) - currentItems.append(ExperimentalUISettings.AccountReactionOverrides.Item( - key: .builtin(stickerName), - messageId: message.id, - mediaId: file.fileId - )) - - settings.accountStickerEffectOverrides.removeAll(where: { $0.accountId == context.account.id.int64 }) - settings.accountStickerEffectOverrides.append(ExperimentalUISettings.AccountReactionOverrides(accountId: context.account.id.int64, items: currentItems)) - - return settings - }).start() - - f(.default) - }))) - } - - return ContextController.Items(content: .list(subActions), disablePositionLock: true, tip: nil) - } - - c.pushItems(items: subItems) - }))) - - actions.append(.separator) - - break - } - } - } } var isDownloading = false @@ -1951,6 +1830,8 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer } else if let story = media as? TelegramMediaStory { if let story = message.associatedStories[story.storyId], story.data.isEmpty { isShareProtected = true + } else if story.isMention { + isShareProtected = true } } } diff --git a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift index 02e1c4b8a7..36aebbb207 100644 --- a/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -338,7 +338,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { title = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) subtitle = nil } - actionTitle = "OPEN STORY" + actionTitle = item.presentationData.strings.Chat_OpenStory default: break } diff --git a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift index 7481e34dab..d832f4dea8 100644 --- a/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift +++ b/submodules/TelegramUI/Sources/ManagedAudioRecorder.swift @@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext { } return ActionDisposable { } - }), playAndRecord: true, ambient: false, mixWithOthers: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe(), updatedRate: { + }), playAndRecord: true, soundMuted: false, ambient: false, mixWithOthers: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe(), updatedRate: { }, audioPaused: {}) self.toneRenderer = toneRenderer diff --git a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift index 13a9f4ab20..a7c1d30bfa 100644 --- a/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift +++ b/submodules/TelegramUI/Sources/OverlayInstantVideoNode.swift @@ -115,6 +115,9 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode { self.videoNode.playOnceWithSound(playAndRecord: playAndRecord) } + func setSoundMuted(soundMuted: Bool) { + } + func continueWithOverridingAmbientMode(isAmbient: Bool) { } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 3831a9f6bc..919793691f 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -2140,6 +2140,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro private var expiringStoryListState: PeerExpiringStoryListContext.State? private var expiringStoryListDisposable: Disposable? + private let storiesReady = ValuePromise(true, ignoreRepeated: true) + private let _ready = Promise() var ready: Promise { return self._ready @@ -3863,6 +3865,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro self?.translationState = translationState }) } else if peerId.namespace == Namespaces.Peer.CloudUser { + self.storiesReady.set(false) let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId) self.expiringStoryList = expiringStoryList self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(), @@ -3897,6 +3900,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro }, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends) } + self.storiesReady.set(true) + self.requestLayout(animated: false) if self.headerNode.avatarListNode.openStories == nil { @@ -9515,10 +9520,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro let avatarReady = self.headerNode.avatarListNode.isReady.get() let combinedSignal = combineLatest(queue: .mainQueue(), avatarReady, + self.storiesReady.get(), self.paneContainerNode.isReady.get() ) - |> map { lhs, rhs in - return lhs && rhs + |> map { a, b, c in + return a && b && c } self._ready.set(combinedSignal |> filter { $0 } diff --git a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift index ca27dd7f71..905d87206b 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/NativeVideoContent.swift @@ -36,6 +36,7 @@ public final class NativeVideoContent: UniversalVideoContent { public let streamVideo: MediaPlayerStreaming public let loopVideo: Bool public let enableSound: Bool + public let soundMuted: Bool public let beginWithAmbientSound: Bool public let mixWithOthers: Bool public let baseRate: Double @@ -55,7 +56,7 @@ public final class NativeVideoContent: UniversalVideoContent { let displayImage: Bool let hasSentFramesToDisplay: (() -> Void)? - public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { + public init(id: NativeVideoContentId, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference? = nil, streamVideo: MediaPlayerStreaming = .none, loopVideo: Bool = false, enableSound: Bool = true, soundMuted: Bool = false, beginWithAmbientSound: Bool = false, mixWithOthers: Bool = false, baseRate: Double = 1.0, fetchAutomatically: Bool = true, onlyFullSizeThumbnail: Bool = false, useLargeThumbnail: Bool = false, autoFetchFullSizeThumbnail: Bool = false, startTimestamp: Double? = nil, endTimestamp: Double? = nil, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor = .white, tempFilePath: String? = nil, isAudioVideoMessage: Bool = false, captureProtected: Bool = false, hintDimensions: CGSize? = nil, storeAfterDownload: (() -> Void)?, displayImage: Bool = true, hasSentFramesToDisplay: (() -> Void)? = nil) { self.id = id self.nativeId = id self.userLocation = userLocation @@ -78,6 +79,7 @@ public final class NativeVideoContent: UniversalVideoContent { self.streamVideo = streamVideo self.loopVideo = loopVideo self.enableSound = enableSound + self.soundMuted = soundMuted self.beginWithAmbientSound = beginWithAmbientSound self.mixWithOthers = mixWithOthers self.baseRate = baseRate @@ -99,7 +101,7 @@ public final class NativeVideoContent: UniversalVideoContent { } public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode { - return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) + return NativeVideoContentNode(postbox: postbox, audioSessionManager: audioSession, userLocation: self.userLocation, fileReference: self.fileReference, imageReference: self.imageReference, streamVideo: self.streamVideo, loopVideo: self.loopVideo, enableSound: self.enableSound, soundMuted: self.soundMuted, beginWithAmbientSound: self.beginWithAmbientSound, mixWithOthers: self.mixWithOthers, baseRate: self.baseRate, fetchAutomatically: self.fetchAutomatically, onlyFullSizeThumbnail: self.onlyFullSizeThumbnail, useLargeThumbnail: self.useLargeThumbnail, autoFetchFullSizeThumbnail: self.autoFetchFullSizeThumbnail, startTimestamp: self.startTimestamp, endTimestamp: self.endTimestamp, continuePlayingWithoutSoundOnLostAudioSession: self.continuePlayingWithoutSoundOnLostAudioSession, placeholderColor: self.placeholderColor, tempFilePath: self.tempFilePath, isAudioVideoMessage: self.isAudioVideoMessage, captureProtected: self.captureProtected, hintDimensions: self.hintDimensions, storeAfterDownload: self.storeAfterDownload, displayImage: self.displayImage, hasSentFramesToDisplay: self.hasSentFramesToDisplay) } public func isEqual(to other: UniversalVideoContent) -> Bool { @@ -121,6 +123,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let userLocation: MediaResourceUserLocation private let fileReference: FileMediaReference private let enableSound: Bool + private let soundMuted: Bool private let beginWithAmbientSound: Bool private let mixWithOthers: Bool private let loopVideo: Bool @@ -180,12 +183,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent private let hasSentFramesToDisplay: (() -> Void)? - init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { + init(postbox: Postbox, audioSessionManager: ManagedAudioSession, userLocation: MediaResourceUserLocation, fileReference: FileMediaReference, imageReference: ImageMediaReference?, streamVideo: MediaPlayerStreaming, loopVideo: Bool, enableSound: Bool, soundMuted: Bool, beginWithAmbientSound: Bool, mixWithOthers: Bool, baseRate: Double, fetchAutomatically: Bool, onlyFullSizeThumbnail: Bool, useLargeThumbnail: Bool, autoFetchFullSizeThumbnail: Bool, startTimestamp: Double?, endTimestamp: Double?, continuePlayingWithoutSoundOnLostAudioSession: Bool = false, placeholderColor: UIColor, tempFilePath: String?, isAudioVideoMessage: Bool, captureProtected: Bool, hintDimensions: CGSize?, storeAfterDownload: (() -> Void)? = nil, displayImage: Bool, hasSentFramesToDisplay: (() -> Void)?) { self.postbox = postbox self.userLocation = userLocation self.fileReference = fileReference self.placeholderColor = placeholderColor self.enableSound = enableSound + self.soundMuted = soundMuted self.beginWithAmbientSound = beginWithAmbientSound self.mixWithOthers = mixWithOthers self.loopVideo = loopVideo @@ -206,7 +210,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent break } - self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) + self.player = MediaPlayer(audioSessionManager: audioSessionManager, postbox: postbox, userLocation: userLocation, userContentType: userContentType, resourceReference: fileReference.resourceReference(fileReference.media.resource), tempFilePath: tempFilePath, streamable: streamVideo, video: true, preferSoftwareDecoding: false, playAutomatically: false, enableSound: enableSound, baseRate: baseRate, fetchAutomatically: fetchAutomatically, soundMuted: soundMuted, ambient: beginWithAmbientSound, mixWithOthers: mixWithOthers, continuePlayingWithoutSoundOnLostAudioSession: continuePlayingWithoutSoundOnLostAudioSession, storeAfterDownload: storeAfterDownload, isAudioVideoMessage: isAudioVideoMessage) var actionAtEndImpl: (() -> Void)? if enableSound && !loopVideo { @@ -483,6 +487,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent self.player.setForceAudioToSpeaker(forceAudioToSpeaker) } + func setSoundMuted(soundMuted: Bool) { + self.player.setSoundMuted(soundMuted: soundMuted) + } + func continueWithOverridingAmbientMode(isAmbient: Bool) { self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient) } diff --git a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift index 9cf34654b6..30bac1bc9c 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/PlatformVideoContent.swift @@ -430,6 +430,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } + func setSoundMuted(soundMuted: Bool) { + } + func continueWithOverridingAmbientMode(isAmbient: Bool) { } diff --git a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift index e473711c37..756fcf51e9 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/SystemVideoContent.swift @@ -267,6 +267,9 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) { } + func setSoundMuted(soundMuted: Bool) { + } + func continueWithOverridingAmbientMode(isAmbient: Bool) { } diff --git a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift index 17acbf9b5b..d45c70364b 100644 --- a/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift +++ b/submodules/TelegramUniversalVideoContent/Sources/WebEmbedVideoContent.swift @@ -164,6 +164,9 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode { } } + func setSoundMuted(soundMuted: Bool) { + } + func continueWithOverridingAmbientMode(isAmbient: Bool) { }