mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
Merge branch 'master' of gitlab.com:peter-iakovlev/telegram-ios
This commit is contained in:
commit
2c6aa07464
@ -1171,6 +1171,7 @@ private final class NotificationServiceHandler {
|
|||||||
}*/
|
}*/
|
||||||
|
|
||||||
if let storyId {
|
if let storyId {
|
||||||
|
content.category = "st"
|
||||||
action = .pollStories(peerId: peerId, content: content, storyId: storyId)
|
action = .pollStories(peerId: peerId, content: content, storyId: storyId)
|
||||||
} else {
|
} else {
|
||||||
action = .poll(peerId: peerId, content: content, messageId: messageIdValue)
|
action = .poll(peerId: peerId, content: content, messageId: messageIdValue)
|
||||||
|
@ -9400,7 +9400,7 @@ Sorry for the inconvenience.";
|
|||||||
"StoryFeed.ContextSavedStories" = "Saved Stories";
|
"StoryFeed.ContextSavedStories" = "Saved Stories";
|
||||||
"StoryFeed.ContextArchivedStories" = "Archived Stories";
|
"StoryFeed.ContextArchivedStories" = "Archived Stories";
|
||||||
"StoryFeed.ContextOpenChat" = "Send Message";
|
"StoryFeed.ContextOpenChat" = "Send Message";
|
||||||
"StoryFeed.ContextOpenProfile" = "View Profile";
|
"StoryFeed.ContextOpenProfile" = "Open Profile";
|
||||||
"StoryFeed.ContextNotifyOn" = "Notify About Stories";
|
"StoryFeed.ContextNotifyOn" = "Notify About Stories";
|
||||||
"StoryFeed.ContextNotifyOff" = "Do Not Notify About Stories";
|
"StoryFeed.ContextNotifyOff" = "Do Not Notify About Stories";
|
||||||
"StoryFeed.ContextArchive" = "Hide 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.UnknownChatsFooter" = "Automatically archive and mute new private chats, groups and channels from non-contacts.";
|
||||||
|
|
||||||
"ArchiveSettings.KeepArchived" = "Always Keep Archived";
|
"ArchiveSettings.KeepArchived" = "Always Keep Archived";
|
||||||
|
"ArchiveSettings.AutomaticallyArchive" = "Automatically Archive";
|
||||||
"ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [Telegram Premium]().";
|
"ArchiveSettings.TooltipPremiumRequired" = "This setting is available only to the subscribers of [Telegram Premium]().";
|
||||||
|
|
||||||
"NotificationSettings.Stories.ShowAll" = "Show All Notifications";
|
"NotificationSettings.Stories.ShowAll" = "Show All Notifications";
|
||||||
@ -9548,10 +9549,9 @@ Sorry for the inconvenience.";
|
|||||||
|
|
||||||
"Story.ContextDeleteStory" = "Delete Story";
|
"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.TooltipPrivacyContacts" = "Only **%@'s** contacts can view this story.";
|
||||||
"Story.TooltipPrivacySelectedContacts" = "Only some contacts **%@** selected can view this story.";
|
"Story.TooltipPrivacySelectedContacts" = "Only some contacts **%@** selected can view this story.";
|
||||||
|
|
||||||
"Story.ToastViewInChat" = "View in Chat";
|
"Story.ToastViewInChat" = "View in Chat";
|
||||||
"Story.ToastReactionSent" = "Reaction Sent.";
|
"Story.ToastReactionSent" = "Reaction Sent.";
|
||||||
|
|
||||||
@ -9618,7 +9618,7 @@ Sorry for the inconvenience.";
|
|||||||
"Story.Editor.DraftDiscardDraft" = "Discard Draft?";
|
"Story.Editor.DraftDiscardDraft" = "Discard Draft?";
|
||||||
"Story.Editor.DraftKeepMedia" = "Save Draft";
|
"Story.Editor.DraftKeepMedia" = "Save Draft";
|
||||||
"Story.Editor.DraftKeepDraft" = "Keep 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.DraftDiscard" = "Discard";
|
||||||
|
|
||||||
"Story.Editor.ExpirationText" = "Choose how long the story will be visible.";
|
"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.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.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.ContactsHeader" = "CONTACTS";
|
||||||
|
|
||||||
"Story.Privacy.SearchChats" = "Search Chats";
|
"Story.Privacy.SearchChats" = "Search Chats";
|
||||||
@ -9712,6 +9712,19 @@ Sorry for the inconvenience.";
|
|||||||
"Story.Privacy.SaveSettings" = "Save Settings";
|
"Story.Privacy.SaveSettings" = "Save Settings";
|
||||||
"Story.Privacy.PostStory" = "Post Story";
|
"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.ViewsExpired" = "List of viewers becomes unavailable **24 hours** after the story expires.";
|
||||||
"Story.Views.NoViews" = "Nobody has viewed\nyour story yet.";
|
"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.";
|
||||||
|
@ -23,6 +23,7 @@ public protocol UniversalVideoContentNode: AnyObject {
|
|||||||
func setSoundEnabled(_ value: Bool)
|
func setSoundEnabled(_ value: Bool)
|
||||||
func seek(_ timestamp: Double)
|
func seek(_ timestamp: Double)
|
||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
||||||
|
func setSoundMuted(soundMuted: Bool)
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool)
|
func continueWithOverridingAmbientMode(isAmbient: Bool)
|
||||||
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
func setForceAudioToSpeaker(_ forceAudioToSpeaker: Bool)
|
||||||
func continuePlayingWithoutSound(actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd)
|
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) {
|
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
self.manager.withUniversalVideoContent(id: self.content.id, { contentNode in
|
||||||
if let contentNode = contentNode {
|
if let contentNode = contentNode {
|
||||||
|
@ -1856,101 +1856,49 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive))
|
if self.previewing {
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] rawStorySubscriptions in
|
self.storiesReady.set(.single(true))
|
||||||
guard let self else {
|
} else {
|
||||||
return
|
self.storySubscriptionsDisposable = (self.context.engine.messages.storySubscriptions(isHidden: self.location == .chatList(groupId: .archive))
|
||||||
}
|
|> deliverOnMainQueue).start(next: { [weak self] rawStorySubscriptions in
|
||||||
|
guard let self else {
|
||||||
self.rawStorySubscriptions = rawStorySubscriptions
|
return
|
||||||
var items: [EngineStorySubscriptions.Item] = []
|
}
|
||||||
if self.shouldFixStorySubscriptionOrder {
|
|
||||||
for peerId in self.fixedStorySubscriptionOrder {
|
self.rawStorySubscriptions = rawStorySubscriptions
|
||||||
if let item = rawStorySubscriptions.items.first(where: { $0.peer.id == peerId }) {
|
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)
|
items.append(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
self.orderedStorySubscriptions = EngineStorySubscriptions(
|
||||||
for item in rawStorySubscriptions.items {
|
accountItem: rawStorySubscriptions.accountItem,
|
||||||
if !items.contains(where: { $0.peer.id == item.peer.id }) {
|
items: items,
|
||||||
items.append(item)
|
hasMoreToken: rawStorySubscriptions.hasMoreToken
|
||||||
}
|
)
|
||||||
}
|
self.fixedStorySubscriptionOrder = items.map(\.peer.id)
|
||||||
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.rawStoryArchiveSubscriptions = rawStoryArchiveSubscriptions
|
let transition: ContainedViewLayoutTransition
|
||||||
|
if self.didAppear {
|
||||||
let archiveStoryState: ChatListNodeState.StoryState?
|
transition = .animated(duration: 0.4, curve: .spring)
|
||||||
if rawStoryArchiveSubscriptions.items.isEmpty {
|
|
||||||
archiveStoryState = nil
|
|
||||||
} else {
|
} else {
|
||||||
var unseenCount = 0
|
transition = .immediate
|
||||||
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
|
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = transition
|
||||||
var chatListState = chatListState
|
self.requestLayout(transition: transition)
|
||||||
|
self.chatListDisplayNode.temporaryContentOffsetChangeTransition = nil
|
||||||
chatListState.archiveStoryState = archiveStoryState
|
|
||||||
|
if !shouldDisplayStoriesInChatListHeader(storySubscriptions: rawStorySubscriptions, isHidden: self.location == .chatList(groupId: .archive)) {
|
||||||
return chatListState
|
self.chatListDisplayNode.scrollToTopIfStoriesAreExpanded()
|
||||||
}
|
}
|
||||||
|
|
||||||
self.storiesReady.set(.single(true))
|
self.storiesReady.set(.single(true))
|
||||||
@ -1961,9 +1909,65 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||||||
}
|
}
|
||||||
self.maybeDisplayStoryTooltip()
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2169,7 +2169,7 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate {
|
|||||||
return
|
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
|
let _ = storySubscriptions
|
||||||
|
|
||||||
self.tempAllowAvatarExpansion = true
|
self.tempAllowAvatarExpansion = true
|
||||||
|
@ -3601,6 +3601,8 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
} else {
|
} else {
|
||||||
strongSelf.view.accessibilityCustomActions = nil
|
strongSelf.view.accessibilityCustomActions = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
strongSelf.avatarTapRecognizer?.isEnabled = item.interaction.inlineNavigationLocation == nil
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1261,7 +1261,7 @@ open class TextNode: ASDisplayNode {
|
|||||||
var additionalTrailingLine: (CTLine, Double)?
|
var additionalTrailingLine: (CTLine, Double)?
|
||||||
|
|
||||||
var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine)
|
var measureFitWidth = CTLineGetTypographicBounds(originalLine, nil, nil, nil) - CTLineGetTrailingWhitespaceWidth(originalLine)
|
||||||
if customTruncationToken != nil {
|
if customTruncationToken != nil && lineRange.location + lineRange.length < attributedString.length {
|
||||||
measureFitWidth += truncationTokenWidth
|
measureFitWidth += truncationTokenWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,15 +27,15 @@ enum MediaPickerGridItemContent: Equatable {
|
|||||||
final class MediaPickerGridItem: GridItem {
|
final class MediaPickerGridItem: GridItem {
|
||||||
let content: MediaPickerGridItemContent
|
let content: MediaPickerGridItemContent
|
||||||
let interaction: MediaPickerInteraction
|
let interaction: MediaPickerInteraction
|
||||||
let strings: PresentationStrings
|
|
||||||
let theme: PresentationTheme
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
let selectable: Bool
|
let selectable: Bool
|
||||||
let enableAnimations: Bool
|
let enableAnimations: Bool
|
||||||
let stories: Bool
|
let stories: Bool
|
||||||
|
|
||||||
let section: GridSection? = nil
|
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.content = content
|
||||||
self.interaction = interaction
|
self.interaction = interaction
|
||||||
self.strings = strings
|
self.strings = strings
|
||||||
@ -57,7 +57,7 @@ final class MediaPickerGridItem: GridItem {
|
|||||||
return node
|
return node
|
||||||
case let .draft(draft, index):
|
case let .draft(draft, index):
|
||||||
let node = MediaPickerGridItemNode()
|
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
|
return node
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +73,7 @@ final class MediaPickerGridItem: GridItem {
|
|||||||
case let .media(media, index):
|
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)
|
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):
|
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.interaction = interaction
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.selectable = selectable
|
self.selectable = selectable
|
||||||
@ -312,7 +312,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.draftNode.supernode == nil {
|
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)
|
self.addSubnode(self.draftNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,8 +61,8 @@ private struct MediaPickerGridEntry: Comparable, Identifiable {
|
|||||||
return lhs.stableId < rhs.stableId
|
return lhs.stableId < rhs.stableId
|
||||||
}
|
}
|
||||||
|
|
||||||
func item(context: AccountContext, interaction: MediaPickerInteraction, strings: PresentationStrings, theme: PresentationTheme) -> MediaPickerGridItem {
|
func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings) -> MediaPickerGridItem {
|
||||||
return MediaPickerGridItem(content: self.content, interaction: interaction, strings: strings, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories)
|
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 updates: [GridNodeUpdateItem]
|
||||||
let scrollToItem: GridNodeScrollToItem?
|
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)
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list)
|
||||||
|
|
||||||
self.deletions = deleteIndices
|
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.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, strings: strings, theme: theme)) }
|
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
|
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)
|
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)
|
self.enqueueTransaction(transaction)
|
||||||
|
|
||||||
if !self.didSetReady {
|
if !self.didSetReady {
|
||||||
|
@ -124,6 +124,7 @@ private final class MediaPlayerContext {
|
|||||||
private var baseRate: Double
|
private var baseRate: Double
|
||||||
private let fetchAutomatically: Bool
|
private let fetchAutomatically: Bool
|
||||||
private var playAndRecord: Bool
|
private var playAndRecord: Bool
|
||||||
|
private var soundMuted: Bool
|
||||||
private var ambient: Bool
|
private var ambient: Bool
|
||||||
private var mixWithOthers: Bool
|
private var mixWithOthers: Bool
|
||||||
private var keepAudioSessionWhilePaused: Bool
|
private var keepAudioSessionWhilePaused: Bool
|
||||||
@ -150,7 +151,7 @@ private final class MediaPlayerContext {
|
|||||||
|
|
||||||
private var stoppedAtEnd = false
|
private var stoppedAtEnd = false
|
||||||
|
|
||||||
init(queue: Queue, audioSessionManager: ManagedAudioSession, playerStatus: Promise<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, 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<MediaPlayerStatus>, audioLevelPipe: ValuePipe<Float>, 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())
|
assert(queue.isCurrent())
|
||||||
|
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
@ -169,6 +170,7 @@ private final class MediaPlayerContext {
|
|||||||
self.baseRate = baseRate
|
self.baseRate = baseRate
|
||||||
self.fetchAutomatically = fetchAutomatically
|
self.fetchAutomatically = fetchAutomatically
|
||||||
self.playAndRecord = playAndRecord
|
self.playAndRecord = playAndRecord
|
||||||
|
self.soundMuted = soundMuted
|
||||||
self.ambient = ambient
|
self.ambient = ambient
|
||||||
self.mixWithOthers = mixWithOthers
|
self.mixWithOthers = mixWithOthers
|
||||||
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
self.keepAudioSessionWhilePaused = keepAudioSessionWhilePaused
|
||||||
@ -404,7 +406,7 @@ private final class MediaPlayerContext {
|
|||||||
self.audioRenderer = nil
|
self.audioRenderer = nil
|
||||||
|
|
||||||
let queue = self.queue
|
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 {
|
queue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.tick()
|
strongSelf.tick()
|
||||||
@ -483,7 +485,7 @@ private final class MediaPlayerContext {
|
|||||||
self.lastStatusUpdateTimestamp = nil
|
self.lastStatusUpdateTimestamp = nil
|
||||||
if self.enableSound {
|
if self.enableSound {
|
||||||
let queue = self.queue
|
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 {
|
queue.async {
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.tick()
|
strongSelf.tick()
|
||||||
@ -601,43 +603,15 @@ private final class MediaPlayerContext {
|
|||||||
self.stoppedAtEnd = false
|
self.stoppedAtEnd = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate func setSoundMuted(soundMuted: Bool) {
|
||||||
|
self.soundMuted = soundMuted
|
||||||
|
self.audioRenderer?.renderer.setSoundMuted(soundMuted: soundMuted)
|
||||||
|
}
|
||||||
|
|
||||||
fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
fileprivate func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
if !isAmbient {
|
if self.ambient != isAmbient {
|
||||||
self.ambient = false
|
self.ambient = isAmbient
|
||||||
var loadedState: MediaPlayerLoadedState?
|
self.audioRenderer?.renderer.reconfigureAudio(ambient: self.ambient)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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
|
let audioLevelPipe = self.audioLevelPipe
|
||||||
self.queue.async {
|
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)
|
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) {
|
public func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
self.queue.async {
|
self.queue.async {
|
||||||
if let context = self.contextRef?.takeUnretainedValue() {
|
if let context = self.contextRef?.takeUnretainedValue() {
|
||||||
|
@ -237,7 +237,9 @@ private final class AudioPlayerRendererContext {
|
|||||||
let audioSessionDisposable = MetaDisposable()
|
let audioSessionDisposable = MetaDisposable()
|
||||||
var audioSessionControl: ManagedAudioSessionControl?
|
var audioSessionControl: ManagedAudioSessionControl?
|
||||||
let playAndRecord: Bool
|
let playAndRecord: Bool
|
||||||
let ambient: Bool
|
var soundMuted: Bool
|
||||||
|
var ambient: Bool
|
||||||
|
var volume: Double = 1.0
|
||||||
let mixWithOthers: Bool
|
let mixWithOthers: Bool
|
||||||
var forceAudioToSpeaker: Bool {
|
var forceAudioToSpeaker: Bool {
|
||||||
didSet {
|
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<Float>, 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<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
||||||
assert(audioPlayerRendererQueue.isCurrent())
|
assert(audioPlayerRendererQueue.isCurrent())
|
||||||
|
|
||||||
self.audioSession = audioSession
|
self.audioSession = audioSession
|
||||||
@ -267,6 +269,7 @@ private final class AudioPlayerRendererContext {
|
|||||||
|
|
||||||
self.playAndRecord = playAndRecord
|
self.playAndRecord = playAndRecord
|
||||||
self.useVoiceProcessingMode = useVoiceProcessingMode
|
self.useVoiceProcessingMode = useVoiceProcessingMode
|
||||||
|
self.soundMuted = soundMuted
|
||||||
self.ambient = ambient
|
self.ambient = ambient
|
||||||
self.mixWithOthers = mixWithOthers
|
self.mixWithOthers = mixWithOthers
|
||||||
|
|
||||||
@ -318,8 +321,10 @@ private final class AudioPlayerRendererContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fileprivate func setVolume(_ volume: Double) {
|
fileprivate func setVolume(_ volume: Double) {
|
||||||
|
self.volume = volume
|
||||||
|
|
||||||
if let mixerAudioUnit = self.mixerAudioUnit {
|
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) {
|
fileprivate func flushBuffers(at timestamp: CMTime, completion: () -> Void) {
|
||||||
assert(audioPlayerRendererQueue.isCurrent())
|
assert(audioPlayerRendererQueue.isCurrent())
|
||||||
|
|
||||||
@ -554,6 +589,8 @@ private final class AudioPlayerRendererContext {
|
|||||||
|
|
||||||
if self.forAudioVideoMessage && !self.ambient {
|
if self.forAudioVideoMessage && !self.ambient {
|
||||||
AudioUnitSetParameter(equalizerAudioUnit, kAUNBandEQParam_GlobalGain, kAudioUnitScope_Global, 0, self.forceAudioToSpeaker ? 0.0 : 12.0, 0)
|
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?
|
var maybeOutputAudioUnit: AudioComponentInstance?
|
||||||
@ -590,6 +627,8 @@ private final class AudioPlayerRendererContext {
|
|||||||
AudioUnitSetProperty(mixerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
|
AudioUnitSetProperty(mixerAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maximumFramesPerSlice, 4)
|
||||||
AudioUnitSetProperty(equalizerAudioUnit, 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)
|
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 {
|
guard AUGraphInitialize(audioGraph) == noErr else {
|
||||||
return
|
return
|
||||||
@ -827,7 +866,7 @@ public final class MediaPlayerAudioRenderer {
|
|||||||
private let audioClock: CMClock
|
private let audioClock: CMClock
|
||||||
public let audioTimebase: CMTimebase
|
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<Float>, 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<Float>, updatedRate: @escaping () -> Void, audioPaused: @escaping () -> Void) {
|
||||||
var audioClock: CMClock?
|
var audioClock: CMClock?
|
||||||
CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
|
CMAudioClockCreate(allocator: nil, clockOut: &audioClock)
|
||||||
if audioClock == nil {
|
if audioClock == nil {
|
||||||
@ -840,7 +879,7 @@ public final class MediaPlayerAudioRenderer {
|
|||||||
self.audioTimebase = audioTimebase!
|
self.audioTimebase = audioTimebase!
|
||||||
|
|
||||||
audioPlayerRendererQueue.async {
|
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)
|
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) {
|
public func setRate(_ rate: Double) {
|
||||||
audioPlayerRendererQueue.async {
|
audioPlayerRendererQueue.async {
|
||||||
if let contextRef = self.contextRef {
|
if let contextRef = self.contextRef {
|
||||||
|
@ -105,7 +105,7 @@ private enum ArchiveSettingsControllerEntry: ItemListNodeEntry {
|
|||||||
case .unknownHeader:
|
case .unknownHeader:
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnknownChatsHeader, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: presentationData.strings.ArchiveSettings_UnknownChatsHeader, sectionId: self.section)
|
||||||
case let .unknownValue(isOn, isLocked):
|
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)
|
arguments.updateUnknown(value)
|
||||||
}, activatedWhileDisabled: {
|
}, activatedWhileDisabled: {
|
||||||
arguments.updateUnknown(nil)
|
arguments.updateUnknown(nil)
|
||||||
|
@ -292,7 +292,7 @@ private func autodownloadMediaConnectionTypeControllerEntries(presentationData:
|
|||||||
|
|
||||||
entries.append(.typesHeader(presentationData.theme, presentationData.strings.AutoDownloadSettings_MediaTypes))
|
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(.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(.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(.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))
|
entries.append(.voiceMessagesInfo(presentationData.theme, presentationData.strings.AutoDownloadSettings_VoiceMessagesInfo))
|
||||||
|
@ -8,7 +8,7 @@ import ComponentFlow
|
|||||||
import MultilineTextComponent
|
import MultilineTextComponent
|
||||||
|
|
||||||
public protocol SparseItemGridLayer: CALayer {
|
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 needsShimmer() -> Bool
|
||||||
|
|
||||||
func getContents() -> Any?
|
func getContents() -> Any?
|
||||||
@ -967,7 +967,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
|
|
||||||
var bindItems: [Item] = []
|
var bindItems: [Item] = []
|
||||||
var bindLayers: [SparseItemGridDisplayItem] = []
|
var bindLayers: [SparseItemGridDisplayItem] = []
|
||||||
var updateLayers: [SparseItemGridDisplayItem] = []
|
var updateLayers: [(SparseItemGridDisplayItem, Int)] = []
|
||||||
|
|
||||||
let addBlur = layout.centerItems
|
let addBlur = layout.centerItems
|
||||||
|
|
||||||
@ -980,7 +980,7 @@ public final class SparseItemGrid: ASDisplayNode {
|
|||||||
let itemLayer: VisibleItem
|
let itemLayer: VisibleItem
|
||||||
if let current = self.visibleItems[item.id] {
|
if let current = self.visibleItems[item.id] {
|
||||||
itemLayer = current
|
itemLayer = current
|
||||||
updateLayers.append(itemLayer)
|
updateLayers.append((itemLayer, index))
|
||||||
} else {
|
} else {
|
||||||
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
itemLayer = VisibleItem(layer: items.itemBinding.createLayer(), view: items.itemBinding.createView())
|
||||||
self.visibleItems[item.id] = itemLayer
|
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)
|
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 item = item as! VisibleItem
|
||||||
|
let contentItem = items.item(at: index)
|
||||||
if let layer = item.layer {
|
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 {
|
} else if let view = item.view {
|
||||||
view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets)
|
view.update(size: view.layer.frame.size, insets: layout.containerLayout.insets)
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ public enum AudioSessionOutputMode: Equatable {
|
|||||||
|
|
||||||
private final class HolderRecord {
|
private final class HolderRecord {
|
||||||
let id: Int32
|
let id: Int32
|
||||||
let audioSessionType: ManagedAudioSessionType
|
var audioSessionType: ManagedAudioSessionType
|
||||||
let control: ManagedAudioSessionControl
|
let control: ManagedAudioSessionControl
|
||||||
let activate: (ManagedAudioSessionControl) -> Void
|
let activate: (ManagedAudioSessionControl) -> Void
|
||||||
let deactivate: (Bool) -> Signal<Void, NoError>
|
let deactivate: (Bool) -> Signal<Void, NoError>
|
||||||
@ -161,12 +161,14 @@ public class ManagedAudioSessionControl {
|
|||||||
private let activateImpl: (ManagedAudioSessionControlActivate) -> Void
|
private let activateImpl: (ManagedAudioSessionControlActivate) -> Void
|
||||||
private let setupAndActivateImpl: (Bool, ManagedAudioSessionControlActivate) -> Void
|
private let setupAndActivateImpl: (Bool, ManagedAudioSessionControlActivate) -> Void
|
||||||
private let setOutputModeImpl: (AudioSessionOutputMode) -> 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.setupImpl = setupImpl
|
||||||
self.activateImpl = activateImpl
|
self.activateImpl = activateImpl
|
||||||
self.setOutputModeImpl = setOutputModeImpl
|
self.setOutputModeImpl = setOutputModeImpl
|
||||||
self.setupAndActivateImpl = setupAndActivateImpl
|
self.setupAndActivateImpl = setupAndActivateImpl
|
||||||
|
self.setTypeImpl = setTypeImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setup(synchronous: Bool = false) {
|
public func setup(synchronous: Bool = false) {
|
||||||
@ -184,6 +186,10 @@ public class ManagedAudioSessionControl {
|
|||||||
public func setOutputMode(_ mode: AudioSessionOutputMode) {
|
public func setOutputMode(_ mode: AudioSessionOutputMode) {
|
||||||
self.setOutputModeImpl(mode)
|
self.setOutputModeImpl(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func setType(_ audioSessionType: ManagedAudioSessionType, completion: @escaping () -> Void) {
|
||||||
|
self.setTypeImpl(audioSessionType, completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public final class ManagedAudioSession: NSObject {
|
public final class ManagedAudioSession: NSObject {
|
||||||
@ -548,6 +554,24 @@ public final class ManagedAudioSession: NSObject {
|
|||||||
queue.async(f)
|
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
|
}), activate: { [weak self] state in
|
||||||
manualActivate(state)
|
manualActivate(state)
|
||||||
queue.async {
|
queue.async {
|
||||||
@ -801,7 +825,11 @@ public final class ManagedAudioSession: NSObject {
|
|||||||
|
|
||||||
switch type {
|
switch type {
|
||||||
case .play(mixWithOthers: true), .ambient:
|
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:
|
default:
|
||||||
break
|
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) {
|
private func updateOutputMode(_ outputMode: AudioSessionOutputMode) {
|
||||||
if let (type, _) = self.currentTypeAndOutputMode {
|
if let (type, _) = self.currentTypeAndOutputMode {
|
||||||
self.setup(type: type, outputMode: outputMode, activateNow: true)
|
self.setup(type: type, outputMode: outputMode, activateNow: true)
|
||||||
|
@ -1158,6 +1158,7 @@ public class Account {
|
|||||||
self.managedOperationsDisposable.add(managedAutoexpireStoryOperations(network: self.network, postbox: self.postbox).start())
|
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(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(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())
|
self.managedOperationsDisposable.add(managedLocalTypingActivities(activities: self.localInputActivityManager.allActivities(), postbox: self.stateManager.postbox, network: self.stateManager.network, accountPeerId: self.stateManager.accountPeerId).start())
|
||||||
|
|
||||||
let extractedExpr: [Signal<AccountRunningImportantTasks, NoError>] = [
|
let extractedExpr: [Signal<AccountRunningImportantTasks, NoError>] = [
|
||||||
|
@ -197,6 +197,7 @@ private var declaredEncodables: Void = {
|
|||||||
declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) })
|
declareEncodable(SynchronizeAutosaveItemOperation.self, f: { SynchronizeAutosaveItemOperation(decoder: $0) })
|
||||||
declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) })
|
declareEncodable(TelegramMediaStory.self, f: { TelegramMediaStory(decoder: $0) })
|
||||||
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
|
declareEncodable(SynchronizeViewStoriesOperation.self, f: { SynchronizeViewStoriesOperation(decoder: $0) })
|
||||||
|
declareEncodable(SynchronizePeerStoriesOperation.self, f: { SynchronizePeerStoriesOperation(decoder: $0) })
|
||||||
return
|
return
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -4489,6 +4489,11 @@ func replayFinalState(
|
|||||||
updatedPeerEntries.append(StoryItemsTableEntry(value: codedEntry, id: storedItem.id, expirationTimestamp: storedItem.expirationTimestamp, isCloseFriends: storedItem.isCloseFriends))
|
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 {
|
} else {
|
||||||
if case let .storyItemDeleted(id) = story {
|
if case let .storyItemDeleted(id) = story {
|
||||||
if let index = updatedPeerEntries.firstIndex(where: { $0.id == id }) {
|
if let index = updatedPeerEntries.firstIndex(where: { $0.id == id }) {
|
||||||
@ -4981,5 +4986,18 @@ func replayFinalState(
|
|||||||
requestChatListFiltersSync(transaction: transaction)
|
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)
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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<Void, NoError>) -> Signal<Void, NoError> {
|
||||||
|
return postbox.transaction { transaction -> Signal<Void, NoError> 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<Void, NoError> {
|
||||||
|
return Signal { _ in
|
||||||
|
let helper = Atomic<ManagedSynchronizePeerStoriesOperationsHelper>(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<Void, NoError> 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<Void, NoError> {
|
||||||
|
return _internal_pollPeerStories(postbox: postbox, network: network, accountPeerId: stateManager.accountPeerId, peerId: peer.id, peerReference: PeerReference(peer))
|
||||||
|
|> map { _ -> Void in
|
||||||
|
}
|
||||||
|
}
|
@ -186,6 +186,7 @@ public struct OperationLogTags {
|
|||||||
public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22)
|
public static let SynchronizeInstalledEmoji = PeerOperationLogTag(value: 22)
|
||||||
public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23)
|
public static let SynchronizeAutosaveItems = PeerOperationLogTag(value: 23)
|
||||||
public static let SynchronizeViewStories = PeerOperationLogTag(value: 24)
|
public static let SynchronizeViewStories = PeerOperationLogTag(value: 24)
|
||||||
|
public static let SynchronizePeerStories = PeerOperationLogTag(value: 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
|
public struct LegacyPeerSummaryCounterTags: OptionSet, Sequence, Hashable {
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
@ -108,6 +108,10 @@ func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId)
|
|||||||
account.stateManager.addUpdates(updates)
|
account.stateManager.addUpdates(updates)
|
||||||
}
|
}
|
||||||
return account.postbox.transaction { transaction -> Void in
|
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()
|
var peerIds = transaction.getContactPeerIds()
|
||||||
if peerIds.contains(peerId) {
|
if peerIds.contains(peerId) {
|
||||||
peerIds.remove(peerId)
|
peerIds.remove(peerId)
|
||||||
|
@ -724,7 +724,7 @@ private func apiInputPrivacyRules(privacy: EngineStoryPrivacy, transaction: Tran
|
|||||||
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
privacyRules = [.inputPrivacyValueAllowCloseFriends]
|
||||||
case .nobody:
|
case .nobody:
|
||||||
if privacy.additionallyIncludePeers.isEmpty {
|
if privacy.additionallyIncludePeers.isEmpty {
|
||||||
privacyRules = [.inputPrivacyValueDisallowAll]
|
privacyRules = [.inputPrivacyValueAllowUsers(users: [.inputUserSelf])]
|
||||||
} else {
|
} else {
|
||||||
privacyRules = []
|
privacyRules = []
|
||||||
}
|
}
|
||||||
|
@ -790,6 +790,44 @@ public final class PeerStoryListContext {
|
|||||||
finalUpdatedState = updatedState
|
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 {
|
} else {
|
||||||
if !self.isArchived {
|
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<Never, NoError> {
|
public func _internal_pollPeerStories(postbox: Postbox, network: Network, accountPeerId: PeerId, peerId: PeerId, peerReference: PeerReference? = nil) -> Signal<Never, NoError> {
|
||||||
return postbox.transaction { transaction -> Api.InputUser? in
|
return postbox.transaction { transaction -> Api.InputUser? in
|
||||||
return transaction.getPeer(peerId).flatMap(apiInputUser)
|
return transaction.getPeer(peerId).flatMap(apiInputUser) ?? peerReference?.inputUser
|
||||||
}
|
}
|
||||||
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
|> mapToSignal { inputUser -> Signal<Never, NoError> in
|
||||||
guard let inputUser = inputUser else {
|
guard let inputUser = inputUser else {
|
||||||
@ -1236,7 +1310,7 @@ public func _internal_pollPeerStories(postbox: Postbox, network: Network, accoun
|
|||||||
|
|
||||||
transaction.setStoryItems(peerId: peerId, items: updatedPeerEntries)
|
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 let user = transaction.getPeer(peerId) as? TelegramUser, let storiesHidden = user.storiesHidden {
|
||||||
if storiesHidden {
|
if storiesHidden {
|
||||||
if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) {
|
if !transaction.storySubscriptionsContains(key: .hidden, peerId: peerId) {
|
||||||
|
@ -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) {
|
func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: AccumulatedPeers) {
|
||||||
var parsedPeers: [Peer] = []
|
var parsedPeers: [Peer] = []
|
||||||
for (_, user) in peers.users {
|
for (_, user) in peers.users {
|
||||||
@ -47,11 +54,17 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
|
|||||||
case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId):
|
case let .user(flags, flags2, _, _, _, _, _, _, _, _, _, _, _, _, _, _, storiesMaxId):
|
||||||
let isMin = (flags & (1 << 20)) != 0
|
let isMin = (flags & (1 << 20)) != 0
|
||||||
let storiesUnavailable = (flags2 & (1 << 4)) != 0
|
let storiesUnavailable = (flags2 & (1 << 4)) != 0
|
||||||
|
|
||||||
if let storiesMaxId = storiesMaxId {
|
if let storiesMaxId = storiesMaxId {
|
||||||
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId)
|
transaction.setStoryItemsInexactMaxId(peerId: user.peerId, id: storiesMaxId)
|
||||||
} else if !isMin && storiesUnavailable {
|
} else if !isMin && storiesUnavailable {
|
||||||
transaction.clearStoryItemsInexactMaxId(peerId: user.peerId)
|
transaction.clearStoryItemsInexactMaxId(peerId: user.peerId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !isMin {
|
||||||
|
let isContact = (flags & (1 << 11)) != 0
|
||||||
|
_internal_updatePeerIsContact(transaction: transaction, user: telegramUser, isContact: isContact)
|
||||||
|
}
|
||||||
case .userEmpty:
|
case .userEmpty:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -65,6 +78,54 @@ func updatePeers(transaction: Transaction, accountPeerId: PeerId, peers: Accumul
|
|||||||
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peers.users)
|
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?) {
|
public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: (Peer?, Peer) -> Peer?) {
|
||||||
transaction.updatePeersInternal(peers, update: { previous, updated in
|
transaction.updatePeersInternal(peers, update: { previous, updated in
|
||||||
let peerId = updated.id
|
let peerId = updated.id
|
||||||
@ -79,32 +140,34 @@ public func updatePeersCustom(transaction: Transaction, peers: [Peer], update: (
|
|||||||
|
|
||||||
switch peerId.namespace {
|
switch peerId.namespace {
|
||||||
case Namespaces.Peer.CloudUser:
|
case Namespaces.Peer.CloudUser:
|
||||||
if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser, let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden {
|
if let updated = updated as? TelegramUser, let previous = previous as? TelegramUser {
|
||||||
if storiesHidden {
|
if let storiesHidden = updated.storiesHidden, storiesHidden != previous.storiesHidden {
|
||||||
if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) {
|
if storiesHidden {
|
||||||
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered)
|
if transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) {
|
||||||
peerIds.removeAll(where: { $0 == updated.id })
|
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered)
|
||||||
transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds)
|
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 !transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) {
|
||||||
if !peerIds.contains(updated.id) {
|
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden)
|
||||||
peerIds.append(updated.id)
|
if !peerIds.contains(updated.id) {
|
||||||
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds)
|
peerIds.append(updated.id)
|
||||||
|
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) {
|
||||||
if transaction.storySubscriptionsContains(key: .hidden, peerId: updated.id) {
|
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden)
|
||||||
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .hidden)
|
peerIds.removeAll(where: { $0 == updated.id })
|
||||||
peerIds.removeAll(where: { $0 == updated.id })
|
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds)
|
||||||
transaction.replaceAllStorySubscriptions(key: .hidden, state: state, peerIds: peerIds)
|
|
||||||
|
if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) {
|
||||||
if !transaction.storySubscriptionsContains(key: .filtered, peerId: updated.id) {
|
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered)
|
||||||
var (state, peerIds) = transaction.getAllStorySubscriptions(key: .filtered)
|
if !peerIds.contains(updated.id) {
|
||||||
if !peerIds.contains(updated.id) {
|
peerIds.append(updated.id)
|
||||||
peerIds.append(updated.id)
|
transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds)
|
||||||
transaction.replaceAllStorySubscriptions(key: .filtered, state: state, peerIds: peerIds)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<EnvironmentType>, transition: Transition) -> CGSize {
|
func update(component: PeerInfoStoryGridScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||||
self.component = component
|
self.component = component
|
||||||
self.state = state
|
self.state = state
|
||||||
@ -500,6 +507,13 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer {
|
|||||||
self.navigationItem.titleView = self.titleView
|
self.navigationItem.titleView = self.titleView
|
||||||
|
|
||||||
self.updateTitle()
|
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) {
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
@ -329,10 +329,14 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
|||||||
return !self.hasContents
|
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 {
|
if let durationLayer = self.durationLayer {
|
||||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
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
|
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 {
|
if let durationLayer = self.durationLayer {
|
||||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
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.itemGridBinding.itemInteraction = self._itemInteraction
|
||||||
|
|
||||||
self.contextGestureContainerNode.isGestureEnabled = true
|
self.contextGestureContainerNode.isGestureEnabled = false
|
||||||
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
self.contextGestureContainerNode.addSubnode(self.itemGrid)
|
||||||
self.addSubnode(self.contextGestureContainerNode)
|
self.addSubnode(self.contextGestureContainerNode)
|
||||||
|
|
||||||
@ -1631,6 +1635,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
gridSnapshot = self.itemGrid.view.snapshotView(afterScreenUpdates: false)
|
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.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 {
|
if let gridSnapshot = gridSnapshot {
|
||||||
self.view.addSubview(gridSnapshot)
|
self.view.addSubview(gridSnapshot)
|
||||||
gridSnapshot.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak gridSnapshot] _ in
|
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)
|
self.itemGrid.addToTransitionSurface(view: view)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
private var gridSelectionGesture: MediaPickerGridSelectionGesture<Int32>?
|
||||||
|
|
||||||
override public func didLoad() {
|
override public func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
/*let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||||
selectionRecognizer.shouldBegin = {
|
selectionRecognizer.shouldBegin = {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
self.view.addGestureRecognizer(selectionRecognizer)
|
self.view.addGestureRecognizer(selectionRecognizer)*/
|
||||||
}
|
}
|
||||||
|
|
||||||
private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])?
|
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 {
|
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 {
|
if location.x < 44.0 {
|
||||||
return false
|
return false
|
||||||
}
|
}*/
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
if gestureRecognizer.state != .failed, let otherGestureRecognizer = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||||
otherGestureRecognizer.isEnabled = false
|
let _ = otherGestureRecognizer
|
||||||
otherGestureRecognizer.isEnabled = true
|
//otherGestureRecognizer.isEnabled = false
|
||||||
|
//otherGestureRecognizer.isEnabled = true
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
return false
|
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)
|
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
|
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 isSelecting {
|
||||||
if self.gridSelectionGesture == nil {
|
if self.gridSelectionGesture == nil {
|
||||||
let selectionGesture = MediaPickerGridSelectionGesture<EngineMessage.Id>()
|
let selectionGesture = MediaPickerGridSelectionGesture<Int32>()
|
||||||
selectionGesture.delegate = self
|
selectionGesture.delegate = self
|
||||||
selectionGesture.sideInset = 44.0
|
selectionGesture.sideInset = 44.0
|
||||||
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
selectionGesture.updateIsScrollEnabled = { [weak self] isEnabled in
|
||||||
self?.itemGrid.isScrollEnabled = isEnabled
|
self?.itemGrid.isScrollEnabled = isEnabled
|
||||||
}
|
}
|
||||||
selectionGesture.itemAt = { [weak self] point in
|
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 {
|
if let strongSelf = self, let itemLayer = strongSelf.itemGrid.item(at: point)?.layer as? ItemLayer, let storyId = itemLayer.item?.story.id {
|
||||||
return (messageId, strongSelf.chatControllerInteraction.selectionState?.selectedIds.contains(messageId) ?? false)
|
return (storyId, strongSelf._itemInteraction?.selectedIds?.contains(storyId) ?? false)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
selectionGesture.updateSelection = { [weak self] messageId, selected in
|
selectionGesture.updateSelection = { [weak self] storyId, selected in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.chatControllerInteraction.toggleMessagesSelection([messageId], selected)
|
strongSelf._itemInteraction?.toggleSelection(storyId, selected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.itemGrid.view.addGestureRecognizer(selectionGesture)
|
self.itemGrid.view.addGestureRecognizer(selectionGesture)
|
||||||
@ -1870,7 +1883,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr
|
|||||||
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
} else if let gridSelectionGesture = self.gridSelectionGesture {
|
||||||
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
self.itemGrid.view.removeGestureRecognizer(gridSelectionGesture)
|
||||||
self.gridSelectionGesture = nil
|
self.gridSelectionGesture = nil
|
||||||
}*/
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateHiddenItems() {
|
private func updateHiddenItems() {
|
||||||
|
@ -473,7 +473,7 @@ private final class GenericItemLayer: CALayer, ItemLayer {
|
|||||||
return !self.hasContents
|
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 {
|
/*if let durationLayer = self.durationLayer {
|
||||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
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
|
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 {
|
/*if let durationLayer = self.durationLayer {
|
||||||
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
durationLayer.frame = CGRect(origin: CGPoint(x: size.width - 3.0, y: size.height - 3.0), size: CGSize())
|
||||||
}*/
|
}*/
|
||||||
|
@ -171,7 +171,7 @@ public extension StoryContainerScreen {
|
|||||||
|> take(1)
|
|> take(1)
|
||||||
|> mapToSignal { state -> Signal<StoryContentContextState, NoError> in
|
|> mapToSignal { state -> Signal<StoryContentContextState, NoError> in
|
||||||
if let slice = state.slice {
|
if let slice = state.slice {
|
||||||
#if DEBUG && true
|
#if DEBUG && false
|
||||||
if "".isEmpty {
|
if "".isEmpty {
|
||||||
return .single(state)
|
return .single(state)
|
||||||
|> delay(4.0, queue: .mainQueue())
|
|> delay(4.0, queue: .mainQueue())
|
||||||
|
@ -914,7 +914,9 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func markAsSeen(id: StoryId) {
|
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) {
|
public func markAsSeen(id: StoryId) {
|
||||||
if self.readGlobally {
|
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) {
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,7 +153,8 @@ final class StoryItemContentComponent: Component {
|
|||||||
imageReference: nil,
|
imageReference: nil,
|
||||||
streamVideo: .story,
|
streamVideo: .story,
|
||||||
loopVideo: true,
|
loopVideo: true,
|
||||||
enableSound: component.audioMode != .off,
|
enableSound: true,
|
||||||
|
soundMuted: component.audioMode == .off,
|
||||||
beginWithAmbientSound: component.audioMode == .ambient,
|
beginWithAmbientSound: component.audioMode == .ambient,
|
||||||
mixWithOthers: true,
|
mixWithOthers: true,
|
||||||
useLargeThumbnail: false,
|
useLargeThumbnail: false,
|
||||||
@ -255,7 +256,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
override func leaveAmbientMode() {
|
override func leaveAmbientMode() {
|
||||||
if let videoNode = self.videoNode {
|
if let videoNode = self.videoNode {
|
||||||
self.ignoreBufferingTimestamp = CFAbsoluteTimeGetCurrent()
|
self.ignoreBufferingTimestamp = CFAbsoluteTimeGetCurrent()
|
||||||
videoNode.setSoundEnabled(true)
|
videoNode.setSoundMuted(soundMuted: false)
|
||||||
videoNode.continueWithOverridingAmbientMode(isAmbient: false)
|
videoNode.continueWithOverridingAmbientMode(isAmbient: false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -266,7 +267,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
if ambient {
|
if ambient {
|
||||||
videoNode.continueWithOverridingAmbientMode(isAmbient: true)
|
videoNode.continueWithOverridingAmbientMode(isAmbient: true)
|
||||||
} else {
|
} else {
|
||||||
videoNode.setSoundEnabled(false)
|
videoNode.setSoundMuted(soundMuted: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,6 @@ final class StoryItemImageView: UIView {
|
|||||||
private(set) var isContentLoaded: Bool = false
|
private(set) var isContentLoaded: Bool = false
|
||||||
var didLoadContents: (() -> Void)?
|
var didLoadContents: (() -> Void)?
|
||||||
|
|
||||||
private var isCaptureProtected: Bool = false
|
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
self.contentView = UIImageView()
|
self.contentView = UIImageView()
|
||||||
self.contentView.contentMode = .scaleAspectFill
|
self.contentView.contentMode = .scaleAspectFill
|
||||||
@ -44,8 +42,8 @@ final class StoryItemImageView: UIView {
|
|||||||
self.disposable?.dispose()
|
self.disposable?.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateImage(image: UIImage) {
|
private func updateImage(image: UIImage, isCaptureProtected: Bool) {
|
||||||
if self.isCaptureProtected {
|
if isCaptureProtected {
|
||||||
let captureProtectedContentLayer: CaptureProtectedContentLayer
|
let captureProtectedContentLayer: CaptureProtectedContentLayer
|
||||||
if let current = self.captureProtectedContentLayer {
|
if let current = self.captureProtectedContentLayer {
|
||||||
captureProtectedContentLayer = current
|
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) {
|
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
|
self.backgroundColor = isCaptureProtected ? UIColor(rgb: 0x181818) : nil
|
||||||
|
|
||||||
var dimensions: CGSize?
|
var dimensions: CGSize?
|
||||||
@ -90,14 +86,28 @@ final class StoryItemImageView: UIView {
|
|||||||
dimensions = representation.dimensions.cgSize
|
dimensions = representation.dimensions.cgSize
|
||||||
|
|
||||||
if isMediaUpdated {
|
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 attemptSynchronous, let path = context.account.postbox.mediaBox.completedResourcePath(id: representation.resource.id, pathExtension: nil) {
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() {
|
if let image = UIImage(contentsOfFile: path)?.preparingForDisplay() {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let image = UIImage(contentsOfFile: path)?.precomposed() {
|
if let image = UIImage(contentsOfFile: path)?.precomposed() {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.isContentLoaded = true
|
self.isContentLoaded = true
|
||||||
@ -105,7 +115,7 @@ final class StoryItemImageView: UIView {
|
|||||||
} else {
|
} else {
|
||||||
if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
if let thumbnailData = image.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
||||||
if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if let image {
|
if let image {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
self.isContentLoaded = true
|
self.isContentLoaded = true
|
||||||
self.didLoadContents?()
|
self.didLoadContents?()
|
||||||
}
|
}
|
||||||
@ -149,16 +159,30 @@ final class StoryItemImageView: UIView {
|
|||||||
dimensions = file.dimensions?.cgSize
|
dimensions = file.dimensions?.cgSize
|
||||||
|
|
||||||
if isMediaUpdated {
|
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())
|
let cachedPath = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedVideoFirstFrameRepresentation())
|
||||||
|
|
||||||
if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) {
|
if attemptSynchronous, FileManager.default.fileExists(atPath: cachedPath) {
|
||||||
if #available(iOS 15.0, *) {
|
if #available(iOS 15.0, *) {
|
||||||
if let image = UIImage(contentsOfFile: cachedPath)?.preparingForDisplay() {
|
if let image = UIImage(contentsOfFile: cachedPath)?.preparingForDisplay() {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let image = UIImage(contentsOfFile: cachedPath)?.precomposed() {
|
if let image = UIImage(contentsOfFile: cachedPath)?.precomposed() {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.isContentLoaded = true
|
self.isContentLoaded = true
|
||||||
@ -166,7 +190,7 @@ final class StoryItemImageView: UIView {
|
|||||||
} else {
|
} else {
|
||||||
if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
if let thumbnailData = file.immediateThumbnailData.flatMap(decodeTinyThumbnail), let thumbnailImage = UIImage(data: thumbnailData) {
|
||||||
if let image = blurredImage(thumbnailImage, radius: 10.0, iterations: 3) {
|
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
|
return
|
||||||
}
|
}
|
||||||
if let image {
|
if let image {
|
||||||
self.updateImage(image: image)
|
self.updateImage(image: image, isCaptureProtected: isCaptureProtected)
|
||||||
self.isContentLoaded = true
|
self.isContentLoaded = true
|
||||||
self.didLoadContents?()
|
self.didLoadContents?()
|
||||||
}
|
}
|
||||||
@ -217,7 +241,7 @@ final class StoryItemImageView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.isCaptureProtected {
|
if isCaptureProtected {
|
||||||
let captureProtectedInfo: ComponentView<Empty>
|
let captureProtectedInfo: ComponentView<Empty>
|
||||||
var captureProtectedInfoTransition = transition
|
var captureProtectedInfoTransition = transition
|
||||||
if let current = self.captureProtectedInfo {
|
if let current = self.captureProtectedInfo {
|
||||||
|
@ -271,6 +271,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
final class VisibleItem {
|
final class VisibleItem {
|
||||||
let externalState = StoryContentItem.ExternalState()
|
let externalState = StoryContentItem.ExternalState()
|
||||||
let contentContainerView: UIView
|
let contentContainerView: UIView
|
||||||
|
let contentTintLayer = SimpleLayer()
|
||||||
let view = ComponentView<StoryContentItem.Environment>()
|
let view = ComponentView<StoryContentItem.Environment>()
|
||||||
var currentProgress: Double = 0.0
|
var currentProgress: Double = 0.0
|
||||||
var isBuffering: Bool = false
|
var isBuffering: Bool = false
|
||||||
@ -282,6 +283,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if #available(iOS 13.0, *) {
|
if #available(iOS 13.0, *) {
|
||||||
self.contentContainerView.layer.cornerCurve = .continuous
|
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 {
|
if self.sendMessageContext.shareController != nil {
|
||||||
return .pause
|
return .pause
|
||||||
}
|
}
|
||||||
|
if self.sendMessageContext.statusController != nil {
|
||||||
|
return .pause
|
||||||
|
}
|
||||||
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
if let navigationController = component.controller()?.navigationController as? NavigationController {
|
||||||
let topViewController = navigationController.topViewController
|
let topViewController = navigationController.topViewController
|
||||||
if !(topViewController is StoryContainerScreen) && !(topViewController is MediaEditorScreen) && !(topViewController is ShareWithPeersScreen) && !(topViewController is AttachmentController) {
|
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 let view = visibleItem.view.view {
|
||||||
if visibleItem.contentContainerView.superview == nil {
|
if visibleItem.contentContainerView.superview == nil {
|
||||||
self.itemsContainerView.addSubview(visibleItem.contentContainerView)
|
self.itemsContainerView.addSubview(visibleItem.contentContainerView)
|
||||||
|
self.itemsContainerView.layer.addSublayer(visibleItem.contentTintLayer)
|
||||||
visibleItem.contentContainerView.addSubview(view)
|
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.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)
|
var transform = CATransform3DMakeScale(itemScale, itemScale, 1.0)
|
||||||
if let pinchState = component.pinchState {
|
if let pinchState = component.pinchState {
|
||||||
let pinchOffset = CGPoint(
|
let pinchOffset = CGPoint(
|
||||||
@ -1154,6 +1164,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
itemTransition.setTransform(view: visibleItem.contentContainerView, transform: transform)
|
itemTransition.setTransform(view: visibleItem.contentContainerView, transform: transform)
|
||||||
itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale))
|
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))
|
let countedFractionDistanceToCenter: CGFloat = max(0.0, min(1.0, unboundFractionDistanceToCenter / 3.0))
|
||||||
var itemAlpha: CGFloat = 1.0 * (1.0 - countedFractionDistanceToCenter) + 0.0 * countedFractionDistanceToCenter
|
var itemAlpha: CGFloat = 1.0 * (1.0 - countedFractionDistanceToCenter) + 0.0 * countedFractionDistanceToCenter
|
||||||
itemAlpha = max(0.0, min(1.0, itemAlpha))
|
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)
|
let collapsedAlpha = itemAlpha * itemLayout.contentScaleFraction + 0.0 * (1.0 - itemLayout.contentScaleFraction)
|
||||||
itemAlpha = (1.0 - fractionDistanceToCenter) * itemAlpha + fractionDistanceToCenter * collapsedAlpha
|
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()
|
var itemProgressMode = self.itemProgressMode()
|
||||||
if index != centralIndex {
|
if index != centralIndex {
|
||||||
@ -1182,6 +1194,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
if !validIds.contains(id) {
|
if !validIds.contains(id) {
|
||||||
removeIds.append(id)
|
removeIds.append(id)
|
||||||
visibleItem.contentContainerView.removeFromSuperview()
|
visibleItem.contentContainerView.removeFromSuperview()
|
||||||
|
visibleItem.contentTintLayer.removeFromSuperlayer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for id in removeIds {
|
for id in removeIds {
|
||||||
@ -1935,7 +1948,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed,
|
wasRecordingDismissed: self.sendMessageContext.wasRecordingDismissed,
|
||||||
timeoutValue: nil,
|
timeoutValue: nil,
|
||||||
timeoutSelected: false,
|
timeoutSelected: false,
|
||||||
displayGradient: false, //(component.inputHeight != 0.0 || inputNodeVisible) && component.metrics.widthClass != .regular,
|
displayGradient: false,
|
||||||
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
bottomInset: component.inputHeight != 0.0 || inputNodeVisible ? 0.0 : bottomContentInset,
|
||||||
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
hideKeyboard: self.sendMessageContext.currentInputMode == .media,
|
||||||
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
forceIsEditing: self.sendMessageContext.currentInputMode == .media,
|
||||||
@ -2506,7 +2519,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let tooltipScreen = TooltipScreen(
|
let tooltipScreen = TooltipScreen(
|
||||||
account: component.context.account,
|
account: component.context.account,
|
||||||
sharedContext: component.context.sharedContext,
|
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)
|
return .dismiss(consume: true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -57,6 +57,7 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
weak var shareController: ShareController?
|
weak var shareController: ShareController?
|
||||||
weak var tooltipScreen: ViewController?
|
weak var tooltipScreen: ViewController?
|
||||||
weak var actionSheet: ViewController?
|
weak var actionSheet: ViewController?
|
||||||
|
weak var statusController: ViewController?
|
||||||
var isViewingAttachedStickers = false
|
var isViewingAttachedStickers = false
|
||||||
|
|
||||||
var currentInputMode: InputMode = .text
|
var currentInputMode: InputMode = .text
|
||||||
@ -363,6 +364,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
controller.present(tooltipScreen, in: .current)
|
controller.present(tooltipScreen, in: .current)
|
||||||
self.tooltipScreen = tooltipScreen
|
self.tooltipScreen = tooltipScreen
|
||||||
view.updateIsProgressPaused()
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
|
HapticFeedback().success()
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) {
|
func presentSendMessageOptions(view: StoryItemSetContainerComponent.View, sourceView: UIView, gesture: ContextGesture?) {
|
||||||
@ -2471,14 +2474,21 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
|
|
||||||
var cancelImpl: (() -> Void)?
|
var cancelImpl: (() -> Void)?
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let progressSignal = Signal<Never, NoError> { [weak parentController] subscriber in
|
let progressSignal = Signal<Never, NoError> { [weak self, weak view, weak parentController] subscriber in
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
cancelImpl?()
|
cancelImpl?()
|
||||||
}))
|
}))
|
||||||
parentController?.present(controller, in: .window(.root))
|
parentController?.present(controller, in: .window(.root))
|
||||||
|
|
||||||
|
self?.statusController = controller
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
|
|
||||||
return ActionDisposable { [weak controller] in
|
return ActionDisposable { [weak controller] in
|
||||||
Queue.mainQueue().async() {
|
Queue.mainQueue().async() {
|
||||||
controller?.dismiss()
|
controller?.dismiss()
|
||||||
|
|
||||||
|
self?.statusController = nil
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2545,14 +2555,21 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
var cancelImpl: (() -> Void)?
|
var cancelImpl: (() -> Void)?
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
let progressSignal = Signal<Never, NoError> { [weak parentController] subscriber in
|
let progressSignal = Signal<Never, NoError> { [weak parentController, weak self, weak view] subscriber in
|
||||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||||
cancelImpl?()
|
cancelImpl?()
|
||||||
}))
|
}))
|
||||||
parentController?.present(controller, in: .window(.root))
|
parentController?.present(controller, in: .window(.root))
|
||||||
|
|
||||||
|
self?.statusController = controller
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
|
|
||||||
return ActionDisposable { [weak controller] in
|
return ActionDisposable { [weak controller] in
|
||||||
Queue.mainQueue().async() {
|
Queue.mainQueue().async() {
|
||||||
controller?.dismiss()
|
controller?.dismiss()
|
||||||
|
|
||||||
|
self?.statusController = nil
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2737,12 +2754,19 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
let context = component.context
|
let context = component.context
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||||
let progressSignal = Signal<Never, NoError> { [weak parentController] subscriber in
|
let progressSignal = Signal<Never, NoError> { [weak parentController, weak self, weak view] subscriber in
|
||||||
let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
let progressController = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: nil))
|
||||||
parentController?.present(progressController, in: .window(.root), with: nil)
|
parentController?.present(progressController, in: .window(.root), with: nil)
|
||||||
|
|
||||||
|
self?.statusController = progressController
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
|
|
||||||
return ActionDisposable { [weak progressController] in
|
return ActionDisposable { [weak progressController] in
|
||||||
Queue.mainQueue().async() {
|
Queue.mainQueue().async() {
|
||||||
progressController?.dismiss()
|
progressController?.dismiss()
|
||||||
|
|
||||||
|
self?.statusController = nil
|
||||||
|
view?.updateIsProgressPaused()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -356,6 +356,8 @@ public final class StoryPeerListComponent: Component {
|
|||||||
public private(set) var overscrollSelectedId: EnginePeer.Id?
|
public private(set) var overscrollSelectedId: EnginePeer.Id?
|
||||||
public private(set) var overscrollHiddenChatItemsAllowed: Bool = false
|
public private(set) var overscrollHiddenChatItemsAllowed: Bool = false
|
||||||
|
|
||||||
|
private var anchorForTooltipRect: CGRect?
|
||||||
|
|
||||||
private var sharedBlurEffect: NSObject?
|
private var sharedBlurEffect: NSObject?
|
||||||
|
|
||||||
public override init(frame: CGRect) {
|
public override init(frame: CGRect) {
|
||||||
@ -482,7 +484,11 @@ public final class StoryPeerListComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func anchorForTooltip() -> (UIView, CGRect)? {
|
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 {
|
public func titleFrame() -> CGRect {
|
||||||
@ -631,6 +637,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
var minFraction: CGFloat
|
var minFraction: CGFloat
|
||||||
var maxFraction: CGFloat
|
var maxFraction: CGFloat
|
||||||
var sideAlphaFraction: CGFloat
|
var sideAlphaFraction: CGFloat
|
||||||
|
var expandEffectFraction: CGFloat
|
||||||
var titleWidth: CGFloat
|
var titleWidth: CGFloat
|
||||||
var activityFraction: CGFloat
|
var activityFraction: CGFloat
|
||||||
}
|
}
|
||||||
@ -668,6 +675,19 @@ public final class StoryPeerListComponent: Component {
|
|||||||
|
|
||||||
let timestamp = CACurrentMediaTime()
|
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 collapsedState: CollapseState
|
||||||
let expandBoundsFraction: CGFloat
|
let expandBoundsFraction: CGFloat
|
||||||
if let animationState = self.animationState {
|
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 animatedTitleWidth = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromTitleWidth, toFraction: realTitleContentWidth)
|
||||||
let animatedActivityFraction = animationState.interpolatedFraction(at: timestamp, effectiveFromFraction: animationState.fromActivityFraction, toFraction: targetActivityFraction)
|
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)
|
var rawProgress = CGFloat((timestamp - animationState.startTime) / animationState.duration)
|
||||||
rawProgress = max(0.0, min(1.0, rawProgress))
|
rawProgress = max(0.0, min(1.0, rawProgress))
|
||||||
|
|
||||||
@ -724,6 +734,17 @@ public final class StoryPeerListComponent: Component {
|
|||||||
} else {
|
} else {
|
||||||
expandBoundsFraction = 0.0
|
expandBoundsFraction = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
collapsedState = CollapseState(
|
||||||
|
globalFraction: animatedGlobalFraction,
|
||||||
|
scaleFraction: animatedScaleFraction,
|
||||||
|
minFraction: animatedMinFraction,
|
||||||
|
maxFraction: animatedMaxFraction,
|
||||||
|
sideAlphaFraction: animatedSideAlphaFraction,
|
||||||
|
expandEffectFraction: calculateOverscrollEffectFraction(animatedMaxFraction, expandBoundsFraction),
|
||||||
|
titleWidth: animatedTitleWidth,
|
||||||
|
activityFraction: animatedActivityFraction
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
collapsedState = CollapseState(
|
collapsedState = CollapseState(
|
||||||
globalFraction: targetFraction,
|
globalFraction: targetFraction,
|
||||||
@ -731,6 +752,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
minFraction: targetMinFraction,
|
minFraction: targetMinFraction,
|
||||||
maxFraction: targetMaxFraction,
|
maxFraction: targetMaxFraction,
|
||||||
sideAlphaFraction: targetSideAlphaFraction,
|
sideAlphaFraction: targetSideAlphaFraction,
|
||||||
|
expandEffectFraction: calculateOverscrollEffectFraction(targetMaxFraction, 0.0),
|
||||||
titleWidth: realTitleContentWidth,
|
titleWidth: realTitleContentWidth,
|
||||||
activityFraction: targetActivityFraction
|
activityFraction: targetActivityFraction
|
||||||
)
|
)
|
||||||
@ -817,7 +839,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//print("overscrollStage2: \(overscrollStage2)")
|
//print("overscrollStage2: \(overscrollStage2)")
|
||||||
if let overscrollFocusIndex, overscrollStage2 >= 1.25 {
|
if let overscrollFocusIndex, overscrollStage2 >= 1.19 {
|
||||||
self.overscrollSelectedId = self.sortedItems[overscrollFocusIndex].peer.id
|
self.overscrollSelectedId = self.sortedItems[overscrollFocusIndex].peer.id
|
||||||
} else {
|
} else {
|
||||||
self.overscrollSelectedId = nil
|
self.overscrollSelectedId = nil
|
||||||
@ -1031,6 +1053,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
scale: itemScale,
|
scale: itemScale,
|
||||||
fullWidth: expandedItemWidth,
|
fullWidth: expandedItemWidth,
|
||||||
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
||||||
|
expandEffectFraction: collapsedState.expandEffectFraction,
|
||||||
leftNeighborDistance: leftNeighborDistance,
|
leftNeighborDistance: leftNeighborDistance,
|
||||||
rightNeighborDistance: rightNeighborDistance,
|
rightNeighborDistance: rightNeighborDistance,
|
||||||
action: component.peerAction,
|
action: component.peerAction,
|
||||||
@ -1166,6 +1189,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
scale: itemScale,
|
scale: itemScale,
|
||||||
fullWidth: expandedItemWidth,
|
fullWidth: expandedItemWidth,
|
||||||
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
expandedAlphaFraction: collapsedState.sideAlphaFraction,
|
||||||
|
expandEffectFraction: collapsedState.expandEffectFraction,
|
||||||
leftNeighborDistance: leftNeighborDistance,
|
leftNeighborDistance: leftNeighborDistance,
|
||||||
rightNeighborDistance: rightNeighborDistance,
|
rightNeighborDistance: rightNeighborDistance,
|
||||||
action: component.peerAction,
|
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)))
|
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
|
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 = 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?
|
var titleIndicatorSize: CGSize?
|
||||||
if collapsedState.activityFraction != 0.0 {
|
if collapsedState.activityFraction != 0.0 {
|
||||||
let collapsedItemMinX = collapsedContentOrigin - collapsedItemWidth * 0.5
|
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(
|
self.animationState = AnimationState(
|
||||||
duration: duration * UIView.animationDurationFactor(),
|
duration: duration * UIView.animationDurationFactor(),
|
||||||
fromIsUnlocked: previousComponent.unlocked,
|
fromIsUnlocked: previousComponent.unlocked,
|
||||||
@ -1491,7 +1523,7 @@ public final class StoryPeerListComponent: Component {
|
|||||||
fromTitleWidth: self.currentTitleWidth,
|
fromTitleWidth: self.currentTitleWidth,
|
||||||
fromActivityFraction: self.currentActivityFraction,
|
fromActivityFraction: self.currentActivityFraction,
|
||||||
startTime: timestamp,
|
startTime: timestamp,
|
||||||
bounce: animationHint?.bounce ?? true
|
bounce: allowBounce
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ private func calculateCircleIntersection(center: CGPoint, otherCenter: CGPoint,
|
|||||||
return (point1Angle, point2Angle)
|
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 leftAngles = leftCenter.flatMap { calculateCircleIntersection(center: center, otherCenter: $0, radius: radius) }
|
||||||
let rightAngles = rightCenter.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)
|
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
|
let endAngle = startAngle + segmentAngle
|
||||||
path.move(to: CGPoint(x: center.x + cos(startAngle) * radius, y: center.y + sin(startAngle) * radius))
|
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 scale: CGFloat
|
||||||
public let fullWidth: CGFloat
|
public let fullWidth: CGFloat
|
||||||
public let expandedAlphaFraction: CGFloat
|
public let expandedAlphaFraction: CGFloat
|
||||||
|
public let expandEffectFraction: CGFloat
|
||||||
public let leftNeighborDistance: CGPoint?
|
public let leftNeighborDistance: CGPoint?
|
||||||
public let rightNeighborDistance: CGPoint?
|
public let rightNeighborDistance: CGPoint?
|
||||||
public let action: (EnginePeer) -> Void
|
public let action: (EnginePeer) -> Void
|
||||||
@ -361,6 +362,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
scale: CGFloat,
|
scale: CGFloat,
|
||||||
fullWidth: CGFloat,
|
fullWidth: CGFloat,
|
||||||
expandedAlphaFraction: CGFloat,
|
expandedAlphaFraction: CGFloat,
|
||||||
|
expandEffectFraction: CGFloat,
|
||||||
leftNeighborDistance: CGPoint?,
|
leftNeighborDistance: CGPoint?,
|
||||||
rightNeighborDistance: CGPoint?,
|
rightNeighborDistance: CGPoint?,
|
||||||
action: @escaping (EnginePeer) -> Void,
|
action: @escaping (EnginePeer) -> Void,
|
||||||
@ -378,6 +380,7 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
self.scale = scale
|
self.scale = scale
|
||||||
self.fullWidth = fullWidth
|
self.fullWidth = fullWidth
|
||||||
self.expandedAlphaFraction = expandedAlphaFraction
|
self.expandedAlphaFraction = expandedAlphaFraction
|
||||||
|
self.expandEffectFraction = expandEffectFraction
|
||||||
self.leftNeighborDistance = leftNeighborDistance
|
self.leftNeighborDistance = leftNeighborDistance
|
||||||
self.rightNeighborDistance = rightNeighborDistance
|
self.rightNeighborDistance = rightNeighborDistance
|
||||||
self.action = action
|
self.action = action
|
||||||
@ -421,6 +424,9 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction {
|
if lhs.expandedAlphaFraction != rhs.expandedAlphaFraction {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.expandEffectFraction != rhs.expandEffectFraction {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.leftNeighborDistance != rhs.leftNeighborDistance {
|
if lhs.leftNeighborDistance != rhs.leftNeighborDistance {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -785,8 +791,8 @@ public final class StoryPeerListItemComponent: Component {
|
|||||||
}
|
}
|
||||||
Transition.immediate.setShapeLayerPath(layer: self.avatarShapeLayer, path: avatarPath)
|
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.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))
|
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
|
let titleString: String
|
||||||
if component.peer.id == component.context.account.peerId {
|
if component.peer.id == component.context.account.peerId {
|
||||||
|
@ -2392,7 +2392,6 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@available(iOS 10.0, *)
|
|
||||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||||
let _ = (accountIdFromNotification(response.notification, sharedContext: self.sharedContextPromise.get())
|
let _ = (accountIdFromNotification(response.notification, sharedContext: self.sharedContextPromise.get())
|
||||||
|> deliverOnMainQueue).start(next: { accountId in
|
|> deliverOnMainQueue).start(next: { accountId in
|
||||||
@ -2493,11 +2492,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
return settings.displayNameOnLockscreen
|
return settings.displayNameOnLockscreen
|
||||||
}
|
}
|
||||||
|> deliverOnMainQueue).start(next: { displayNames in
|
|> 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()
|
let notificationCenter = UNUserNotificationCenter.current()
|
||||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
|
||||||
notificationCenter.getNotificationSettings(completionHandler: { settings in
|
notificationCenter.getNotificationSettings(completionHandler: { settings in
|
||||||
@ -2527,38 +2526,28 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
let groupRepliableMediaMessageCategory: UNNotificationCategory
|
let groupRepliableMediaMessageCategory: UNNotificationCategory
|
||||||
let channelMessageCategory: UNNotificationCategory
|
let channelMessageCategory: UNNotificationCategory
|
||||||
let reactionMessageCategory: UNNotificationCategory
|
let reactionMessageCategory: UNNotificationCategory
|
||||||
|
let storyCategory: UNNotificationCategory
|
||||||
|
|
||||||
if #available(iOS 11.0, *) {
|
var options: UNNotificationCategoryOptions = []
|
||||||
var options: UNNotificationCategoryOptions = []
|
if includeNames {
|
||||||
if includeNames {
|
options.insert(.hiddenPreviewsShowTitle)
|
||||||
options.insert(.hiddenPreviewsShowTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
var carPlayOptions = options
|
|
||||||
carPlayOptions.insert(.allowInCarPlay)
|
|
||||||
if #available(iOS 13.2, *) {
|
|
||||||
carPlayOptions.insert(.allowAnnouncement)
|
|
||||||
}
|
|
||||||
|
|
||||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
||||||
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
|
||||||
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
|
||||||
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
||||||
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
||||||
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
|
||||||
reactionMessageCategory = UNNotificationCategory(identifier: "t", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenReactionContentString, options: options)
|
|
||||||
} else {
|
|
||||||
let carPlayOptions: UNNotificationCategoryOptions = [.allowInCarPlay]
|
|
||||||
|
|
||||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: [])
|
|
||||||
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)
|
|
||||||
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], options: [])
|
|
||||||
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: [])
|
|
||||||
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], options: [])
|
|
||||||
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], options: [])
|
|
||||||
reactionMessageCategory = UNNotificationCategory(identifier: "t", actions: [], intentIdentifiers: [], options: [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var carPlayOptions = options
|
||||||
|
carPlayOptions.insert(.allowInCarPlay)
|
||||||
|
if #available(iOS 13.2, *) {
|
||||||
|
carPlayOptions.insert(.allowAnnouncement)
|
||||||
|
}
|
||||||
|
|
||||||
|
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||||
|
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||||
|
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||||
|
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||||
|
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||||
|
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||||
|
reactionMessageCategory = UNNotificationCategory(identifier: "t", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenReactionContentString, options: options)
|
||||||
|
storyCategory = UNNotificationCategory(identifier: "st", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenStoryContentString, options: options)
|
||||||
|
|
||||||
UNUserNotificationCenter.current().setNotificationCategories([
|
UNUserNotificationCenter.current().setNotificationCategories([
|
||||||
unknownMessageCategory,
|
unknownMessageCategory,
|
||||||
repliableMessageCategory,
|
repliableMessageCategory,
|
||||||
@ -2566,7 +2555,8 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
|||||||
channelMessageCategory,
|
channelMessageCategory,
|
||||||
reactionMessageCategory,
|
reactionMessageCategory,
|
||||||
groupRepliableMessageCategory,
|
groupRepliableMessageCategory,
|
||||||
groupRepliableMediaMessageCategory
|
groupRepliableMediaMessageCategory,
|
||||||
|
storyCategory
|
||||||
])
|
])
|
||||||
|
|
||||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
|
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
|
||||||
|
@ -4524,7 +4524,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if let story = message.associatedStories[storyId], story.data.isEmpty {
|
if let story = message.associatedStories[storyId], story.data.isEmpty {
|
||||||
self.present(UndoOverlayController(presentationData: self.presentationData, content: .info(title: nil, text: self.presentationData.strings.Story_TooltipExpired, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
|
self.present(UndoOverlayController(presentationData: self.presentationData, content: .universal(animation: "story_expired", scale: 0.066, colors: [:], title: nil, text: self.presentationData.strings.Story_TooltipExpired, customUndoText: nil, timeout: nil), elevatedLayout: false, action: { _ in return true }), in: .current)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12246,7 +12246,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
|
|
||||||
@objc func rightNavigationButtonAction() {
|
@objc func rightNavigationButtonAction() {
|
||||||
if let button = self.rightNavigationButton {
|
if let button = self.rightNavigationButton {
|
||||||
if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true) = button.action, let storyStats = self.storyStats, storyStats.totalCount != 0, let avatarNode = self.avatarNode {
|
if case let .peer(peerId) = self.chatLocation, case .openChatInfo(expandAvatar: true) = button.action, let storyStats = self.storyStats, storyStats.unseenCount != 0, let avatarNode = self.avatarNode {
|
||||||
self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode)
|
self.openStories(peerId: peerId, avatarHeaderNode: nil, avatarNode: avatarNode.avatarNode)
|
||||||
} else {
|
} else {
|
||||||
self.navigationButtonAction(button.action)
|
self.navigationButtonAction(button.action)
|
||||||
@ -19170,6 +19170,10 @@ func canAddMessageReactions(message: Message) -> Bool {
|
|||||||
for media in message.media {
|
for media in message.media {
|
||||||
if let _ = media as? TelegramMediaAction {
|
if let _ = media as? TelegramMediaAction {
|
||||||
return false
|
return false
|
||||||
|
} else if let story = media as? TelegramMediaStory {
|
||||||
|
if story.isMention {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -151,6 +151,9 @@ private func canEditMessage(accountPeerId: PeerId, limitsConfiguration: EngineCo
|
|||||||
} else if let _ = media as? TelegramMediaInvoice {
|
} else if let _ = media as? TelegramMediaInvoice {
|
||||||
hasUneditableAttributes = true
|
hasUneditableAttributes = true
|
||||||
break
|
break
|
||||||
|
} else if let _ = media as? TelegramMediaStory {
|
||||||
|
hasUneditableAttributes = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,6 +565,10 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
|||||||
}
|
}
|
||||||
} else if let dice = media as? TelegramMediaDice {
|
} else if let dice = media as? TelegramMediaDice {
|
||||||
diceEmoji = dice.emoji
|
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 = media as? TelegramMediaStory {
|
||||||
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
||||||
canPin = false
|
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<ContextController.Items, NoError> = 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<ContextController.Items, NoError> = 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
|
var isDownloading = false
|
||||||
@ -1951,6 +1830,8 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
|||||||
} else if let story = media as? TelegramMediaStory {
|
} else if let story = media as? TelegramMediaStory {
|
||||||
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
if let story = message.associatedStories[story.storyId], story.data.isEmpty {
|
||||||
isShareProtected = true
|
isShareProtected = true
|
||||||
|
} else if story.isMention {
|
||||||
|
isShareProtected = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -338,7 +338,7 @@ final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
title = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
title = EnginePeer(peer).displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||||
subtitle = nil
|
subtitle = nil
|
||||||
}
|
}
|
||||||
actionTitle = "OPEN STORY"
|
actionTitle = item.presentationData.strings.Chat_OpenStory
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -214,7 +214,7 @@ final class ManagedAudioRecorderContext {
|
|||||||
}
|
}
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
}
|
}
|
||||||
}), playAndRecord: true, ambient: false, mixWithOthers: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: {
|
}), playAndRecord: true, soundMuted: false, ambient: false, mixWithOthers: false, forceAudioToSpeaker: false, baseRate: 1.0, audioLevelPipe: ValuePipe<Float>(), updatedRate: {
|
||||||
}, audioPaused: {})
|
}, audioPaused: {})
|
||||||
self.toneRenderer = toneRenderer
|
self.toneRenderer = toneRenderer
|
||||||
|
|
||||||
|
@ -115,6 +115,9 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
|
|||||||
self.videoNode.playOnceWithSound(playAndRecord: playAndRecord)
|
self.videoNode.playOnceWithSound(playAndRecord: playAndRecord)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2140,6 +2140,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
private var expiringStoryListState: PeerExpiringStoryListContext.State?
|
private var expiringStoryListState: PeerExpiringStoryListContext.State?
|
||||||
private var expiringStoryListDisposable: Disposable?
|
private var expiringStoryListDisposable: Disposable?
|
||||||
|
|
||||||
|
private let storiesReady = ValuePromise<Bool>(true, ignoreRepeated: true)
|
||||||
|
|
||||||
private let _ready = Promise<Bool>()
|
private let _ready = Promise<Bool>()
|
||||||
var ready: Promise<Bool> {
|
var ready: Promise<Bool> {
|
||||||
return self._ready
|
return self._ready
|
||||||
@ -3863,6 +3865,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
self?.translationState = translationState
|
self?.translationState = translationState
|
||||||
})
|
})
|
||||||
} else if peerId.namespace == Namespaces.Peer.CloudUser {
|
} else if peerId.namespace == Namespaces.Peer.CloudUser {
|
||||||
|
self.storiesReady.set(false)
|
||||||
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
|
let expiringStoryList = PeerExpiringStoryListContext(account: context.account, peerId: peerId)
|
||||||
self.expiringStoryList = expiringStoryList
|
self.expiringStoryList = expiringStoryList
|
||||||
self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(),
|
self.expiringStoryListDisposable = (combineLatest(queue: .mainQueue(),
|
||||||
@ -3897,6 +3900,8 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
}, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends)
|
}, state.items.count, state.hasUnseen, state.hasUnseenCloseFriends)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.storiesReady.set(true)
|
||||||
|
|
||||||
self.requestLayout(animated: false)
|
self.requestLayout(animated: false)
|
||||||
|
|
||||||
if self.headerNode.avatarListNode.openStories == nil {
|
if self.headerNode.avatarListNode.openStories == nil {
|
||||||
@ -9515,10 +9520,11 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
|||||||
let avatarReady = self.headerNode.avatarListNode.isReady.get()
|
let avatarReady = self.headerNode.avatarListNode.isReady.get()
|
||||||
let combinedSignal = combineLatest(queue: .mainQueue(),
|
let combinedSignal = combineLatest(queue: .mainQueue(),
|
||||||
avatarReady,
|
avatarReady,
|
||||||
|
self.storiesReady.get(),
|
||||||
self.paneContainerNode.isReady.get()
|
self.paneContainerNode.isReady.get()
|
||||||
)
|
)
|
||||||
|> map { lhs, rhs in
|
|> map { a, b, c in
|
||||||
return lhs && rhs
|
return a && b && c
|
||||||
}
|
}
|
||||||
self._ready.set(combinedSignal
|
self._ready.set(combinedSignal
|
||||||
|> filter { $0 }
|
|> filter { $0 }
|
||||||
|
@ -36,6 +36,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
public let streamVideo: MediaPlayerStreaming
|
public let streamVideo: MediaPlayerStreaming
|
||||||
public let loopVideo: Bool
|
public let loopVideo: Bool
|
||||||
public let enableSound: Bool
|
public let enableSound: Bool
|
||||||
|
public let soundMuted: Bool
|
||||||
public let beginWithAmbientSound: Bool
|
public let beginWithAmbientSound: Bool
|
||||||
public let mixWithOthers: Bool
|
public let mixWithOthers: Bool
|
||||||
public let baseRate: Double
|
public let baseRate: Double
|
||||||
@ -55,7 +56,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
let displayImage: Bool
|
let displayImage: Bool
|
||||||
let hasSentFramesToDisplay: (() -> Void)?
|
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.id = id
|
||||||
self.nativeId = id
|
self.nativeId = id
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
@ -78,6 +79,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
self.streamVideo = streamVideo
|
self.streamVideo = streamVideo
|
||||||
self.loopVideo = loopVideo
|
self.loopVideo = loopVideo
|
||||||
self.enableSound = enableSound
|
self.enableSound = enableSound
|
||||||
|
self.soundMuted = soundMuted
|
||||||
self.beginWithAmbientSound = beginWithAmbientSound
|
self.beginWithAmbientSound = beginWithAmbientSound
|
||||||
self.mixWithOthers = mixWithOthers
|
self.mixWithOthers = mixWithOthers
|
||||||
self.baseRate = baseRate
|
self.baseRate = baseRate
|
||||||
@ -99,7 +101,7 @@ public final class NativeVideoContent: UniversalVideoContent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func makeContentNode(postbox: Postbox, audioSession: ManagedAudioSession) -> UniversalVideoContentNode & ASDisplayNode {
|
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 {
|
public func isEqual(to other: UniversalVideoContent) -> Bool {
|
||||||
@ -121,6 +123,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
private let userLocation: MediaResourceUserLocation
|
private let userLocation: MediaResourceUserLocation
|
||||||
private let fileReference: FileMediaReference
|
private let fileReference: FileMediaReference
|
||||||
private let enableSound: Bool
|
private let enableSound: Bool
|
||||||
|
private let soundMuted: Bool
|
||||||
private let beginWithAmbientSound: Bool
|
private let beginWithAmbientSound: Bool
|
||||||
private let mixWithOthers: Bool
|
private let mixWithOthers: Bool
|
||||||
private let loopVideo: Bool
|
private let loopVideo: Bool
|
||||||
@ -180,12 +183,13 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
|
|
||||||
private let hasSentFramesToDisplay: (() -> Void)?
|
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.postbox = postbox
|
||||||
self.userLocation = userLocation
|
self.userLocation = userLocation
|
||||||
self.fileReference = fileReference
|
self.fileReference = fileReference
|
||||||
self.placeholderColor = placeholderColor
|
self.placeholderColor = placeholderColor
|
||||||
self.enableSound = enableSound
|
self.enableSound = enableSound
|
||||||
|
self.soundMuted = soundMuted
|
||||||
self.beginWithAmbientSound = beginWithAmbientSound
|
self.beginWithAmbientSound = beginWithAmbientSound
|
||||||
self.mixWithOthers = mixWithOthers
|
self.mixWithOthers = mixWithOthers
|
||||||
self.loopVideo = loopVideo
|
self.loopVideo = loopVideo
|
||||||
@ -206,7 +210,7 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
break
|
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)?
|
var actionAtEndImpl: (() -> Void)?
|
||||||
if enableSound && !loopVideo {
|
if enableSound && !loopVideo {
|
||||||
@ -483,6 +487,10 @@ private final class NativeVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
self.player.setForceAudioToSpeaker(forceAudioToSpeaker)
|
self.player.setForceAudioToSpeaker(forceAudioToSpeaker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
|
self.player.setSoundMuted(soundMuted: soundMuted)
|
||||||
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient)
|
self.player.continueWithOverridingAmbientMode(isAmbient: isAmbient)
|
||||||
}
|
}
|
||||||
|
@ -430,6 +430,9 @@ private final class PlatformVideoContentNode: ASDisplayNode, UniversalVideoConte
|
|||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,6 +267,9 @@ private final class SystemVideoContentNode: ASDisplayNode, UniversalVideoContent
|
|||||||
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
func playOnceWithSound(playAndRecord: Bool, seek: MediaPlayerSeek, actionAtEnd: MediaPlayerPlayOnceWithSoundActionAtEnd) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,6 +164,9 @@ final class WebEmbedVideoContentNode: ASDisplayNode, UniversalVideoContentNode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSoundMuted(soundMuted: Bool) {
|
||||||
|
}
|
||||||
|
|
||||||
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
func continueWithOverridingAmbientMode(isAmbient: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user