From ae49ad3700629997eef6a5333e40fb0f0581b4be Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 1 Aug 2023 19:57:08 +0300 Subject: [PATCH] Stories --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 + .../Sources/ChatListSearchListPaneNode.swift | 108 +++++++----- .../Sources/ChatListSearchMediaNode.swift | 2 +- .../Display/Source/LinkHighlightingNode.swift | 2 +- .../Sources/HashtagSearchController.swift | 2 +- .../Sources/ReactionContextNode.swift | 8 +- .../TelegramEngine/Messages/Stories.swift | 111 ++++++++++-- .../Messages/StoryListContext.swift | 31 +++- .../Messages/TelegramEngineMessages.swift | 7 +- .../Resources/PresentationResourceKey.swift | 2 + .../Resources/PresentationResourcesChat.swift | 6 + .../Sources/MediaEditorScreen.swift | 2 + .../Sources/StoryPreviewComponent.swift | 2 + .../MessageInputActionButtonComponent.swift | 19 +- .../Sources/MessageInputPanelComponent.swift | 102 +++++++++-- .../Sources/PeerInfoStoryGridScreen.swift | 43 ++--- .../Sources/PeerListItemComponent.swift | 35 +++- .../Sources/StoryChatContent.swift | 9 +- .../StoryContentCaptionComponent.swift | 21 ++- .../StoryItemSetContainerComponent.swift | 164 +++++++++++++++--- .../StoryItemSetViewListComponent.swift | 1 + .../Sources/StoryFooterPanelComponent.swift | 2 +- .../InputLikeOff.imageset/Contents.json | 12 ++ .../Stories/InputLikeOff.imageset/like_30.pdf | 135 ++++++++++++++ .../InputLikeOn.imageset/Contents.json | 12 ++ .../InputLikeOn.imageset/heartfilled_30.pdf | 75 ++++++++ .../Sources/ChatMessageBubbleItemNode.swift | 9 +- .../ChatMessageTextBubbleContentNode.swift | 12 +- .../Sources/TextSelectionNode.swift | 17 +- 29 files changed, 805 insertions(+), 149 deletions(-) create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/like_30.pdf create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/Contents.json create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/heartfilled_30.pdf diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 574efdf128..8c36053a3b 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9747,3 +9747,6 @@ Sorry for the inconvenience."; "Story.Privacy.HideMyStoriesFrom" = "Hide My Stories From"; "Story.Privacy.SaveList" = "Save List"; + +"StoryList.TooltipStoriesSavedToProfile_1" = "Story archived"; +"StoryList.TooltipStoriesSavedToProfile_any" = "%d stories archived."; diff --git a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift index e439f42aa0..22a4d839c5 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchListPaneNode.swift @@ -315,23 +315,23 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } case topic(EnginePeer, ChatListItemContent.ThreadInfo, Int, PresentationTheme, PresentationStrings, ChatListSearchSectionExpandType) - case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder) - case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) - case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType) - case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool) + case recentlySearchedPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, PeerStoryStats?) + case localPeer(EnginePeer, EnginePeer?, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?) + case globalPeer(FoundPeer, (Int32, Bool)?, Int, PresentationTheme, PresentationStrings, PresentationPersonNameOrder, PresentationPersonNameOrder, ChatListSearchSectionExpandType, PeerStoryStats?) + case message(EngineMessage, EngineRenderedPeer, EnginePeerReadCounters?, EngineMessageHistoryThread.Info?, ChatListPresentationData, Int32, Bool?, Bool, MessageOrderingKey, (id: String, size: Int64, isFirstInList: Bool)?, MessageSection, Bool, PeerStoryStats?) case addContact(String, PresentationTheme, PresentationStrings) public var stableId: ChatListSearchEntryStableId { switch self { case let .topic(_, threadInfo, _, _, _, _): return .threadId(threadInfo.id) - case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _): + case let .recentlySearchedPeer(peer, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .localPeer(peer, _, _, _, _, _, _, _, _): + case let .localPeer(peer, _, _, _, _, _, _, _, _, _): return .localPeerId(peer.id) - case let .globalPeer(peer, _, _, _, _, _, _, _): + case let .globalPeer(peer, _, _, _, _, _, _, _, _): return .globalPeerId(peer.peer.id) - case let .message(message, _, _, _, _, _, _, _, _, _, section, _): + case let .message(message, _, _, _, _, _, _, _, _, _, section, _, _): return .messageId(message.id, section) case .addContact: return .addContact @@ -346,26 +346,26 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return false } - case let .recentlySearchedPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder): - if case let .recentlySearchedPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 { + case let .recentlySearchedPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsStoryStats): + if case let .recentlySearchedPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsStoryStats) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsStoryStats == rhsStoryStats { return true } else { return false } - case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): - if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { + case let .localPeer(lhsPeer, lhsAssociatedPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats): + if case let .localPeer(rhsPeer, rhsAssociatedPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats) = rhs, lhsPeer == rhsPeer && lhsAssociatedPeer == rhsAssociatedPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats { return true } else { return false } - case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType): - if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType { + case let .globalPeer(lhsPeer, lhsUnreadBadge, lhsIndex, lhsTheme, lhsStrings, lhsSortOrder, lhsDisplayOrder, lhsExpandType, lhsStoryStats): + if case let .globalPeer(rhsPeer, rhsUnreadBadge, rhsIndex, rhsTheme, rhsStrings, rhsSortOrder, rhsDisplayOrder, rhsExpandType, rhsStoryStats) = rhs, lhsPeer == rhsPeer && lhsIndex == rhsIndex && lhsTheme === rhsTheme && lhsStrings === rhsStrings && lhsSortOrder == rhsSortOrder && lhsDisplayOrder == rhsDisplayOrder && lhsUnreadBadge?.0 == rhsUnreadBadge?.0 && lhsUnreadBadge?.1 == rhsUnreadBadge?.1 && lhsExpandType == rhsExpandType && lhsStoryStats == rhsStoryStats { return true } else { return false } - case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsThreadInfo, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused): - if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsThreadInfo, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused) = rhs { + case let .message(lhsMessage, lhsPeer, lhsCombinedPeerReadState, lhsThreadInfo, lhsPresentationData, lhsTotalCount, lhsSelected, lhsDisplayCustomHeader, lhsKey, lhsResourceId, lhsSection, lhsAllPaused, lhsStoryStats): + if case let .message(rhsMessage, rhsPeer, rhsCombinedPeerReadState, rhsThreadInfo, rhsPresentationData, rhsTotalCount, rhsSelected, rhsDisplayCustomHeader, rhsKey, rhsResourceId, rhsSection, rhsAllPaused, rhsStoryStats) = rhs { if lhsMessage.id != rhsMessage.id { return false } @@ -408,6 +408,9 @@ public enum ChatListSearchEntry: Comparable, Identifiable { if lhsAllPaused != rhsAllPaused { return false } + if lhsStoryStats != rhsStoryStats { + return false + } return true } else { return false @@ -438,34 +441,34 @@ public enum ChatListSearchEntry: Comparable, Identifiable { } else { return true } - case let .recentlySearchedPeer(_, _, _, lhsIndex, _, _, _, _): + case let .recentlySearchedPeer(_, _, _, lhsIndex, _, _, _, _, _): if case .topic = rhs { return false - } else if case let .recentlySearchedPeer(_, _, _, rhsIndex, _, _, _, _) = rhs { + } else if case let .recentlySearchedPeer(_, _, _, rhsIndex, _, _, _, _, _) = rhs { return lhsIndex <= rhsIndex } else { return true } - case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _): + case let .localPeer(_, _, _, lhsIndex, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer: return false - case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _): + case let .localPeer(_, _, _, rhsIndex, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .globalPeer, .message, .addContact: return true } - case let .globalPeer(_, _, lhsIndex, _, _, _, _, _): + case let .globalPeer(_, _, lhsIndex, _, _, _, _, _, _): switch rhs { case .topic, .recentlySearchedPeer, .localPeer: return false - case let .globalPeer(_, _, rhsIndex, _, _, _, _, _): + case let .globalPeer(_, _, rhsIndex, _, _, _, _, _, _): return lhsIndex <= rhsIndex case .message, .addContact: return true } - case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _): - if case let .message(_, _, _, _, _, _, _, _, rhsKey, _, _, _) = rhs { + case let .message(_, _, _, _, _, _, _, _, lhsKey, _, _, _, _): + if case let .message(_, _, _, _, _, _, _, _, rhsKey, _, _, _, _) = rhs { return lhsKey < rhsKey } else if case .addContact = rhs { return true @@ -496,7 +499,7 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return ContactsPeerItem(presentationData: ItemListPresentationData(presentationData), sortOrder: .firstLast, displayOrder: .firstLast, context: context, peerMode: .generalSearch, peer: .thread(peer: peer, title: threadInfo.info.title, icon: threadInfo.info.icon, color: threadInfo.info.iconColor), status: .none, badge: nil, enabled: true, selection: .none, editing: ContactsPeerItemEditing(editable: false, editing: false, revealed: false), index: nil, header: header, action: { _ in interaction.peerSelected(peer, nil, threadInfo.id, nil) }, contextAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) - case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder): + case let .recentlySearchedPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, storyStats): let primaryPeer: EnginePeer var chatPeer: EnginePeer? if let associatedPeer = associatedPeer { @@ -571,8 +574,10 @@ public enum ChatListSearchEntry: Comparable, Identifiable { gesture?.cancel() } } - }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) - case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): + }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in + return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends) + }) + case let .localPeer(peer, associatedPeer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats): let primaryPeer: EnginePeer var chatPeer: EnginePeer? if let associatedPeer = associatedPeer { @@ -662,8 +667,10 @@ public enum ChatListSearchEntry: Comparable, Identifiable { gesture?.cancel() } } - }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) - case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType): + }, arrowAction: nil, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in + return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends) + }) + case let .globalPeer(peer, unreadBadge, _, theme, strings, nameSortOrder, nameDisplayOrder, expandType, storyStats): var enabled = true if filter.contains(.onlyWriteable) { enabled = canSendMessagesToPeer(peer.peer) @@ -721,8 +728,10 @@ public enum ChatListSearchEntry: Comparable, Identifiable { return { node, gesture, location in peerContextAction(EnginePeer(peer.peer), .search(nil), node, gesture, location) } - }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer) - case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused): + }, animationCache: interaction.animationCache, animationRenderer: interaction.animationRenderer, storyStats: storyStats.flatMap { stats in + return (stats.totalCount, stats.unseenCount, stats.hasUnseenCloseFriends) + }) + case let .message(message, peer, readState, threadInfo, presentationData, _, selected, displayCustomHeader, orderingKey, _, _, allPaused, storyStats): let header: ChatListSearchItemHeader switch orderingKey { case .downloading: @@ -798,7 +807,16 @@ public enum ChatListSearchEntry: Comparable, Identifiable { forumTopicData: nil, topForumTopicItems: [], autoremoveTimeout: nil, - storyState: nil + storyState: storyStats.flatMap { stats in + return ChatListItemContent.StoryState( + stats: EngineChatList.StoryStats( + totalCount: stats.totalCount, + unseenCount: stats.unseenCount, + hasUnseenCloseFriends: stats.hasUnseenCloseFriends + ), + hasUnseenCloseFriends: stats.hasUnseenCloseFriends + ) + } )), editing: false, hasActiveRevealControls: false, selected: false, header: tagMask == nil ? header : nil, enableContextActions: false, hiddenOffset: false, interaction: interaction) } case let .addContact(phoneNumber, theme, strings): @@ -1317,7 +1335,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { resource = (resourceValue.id.stringRepresentation, size, entries.isEmpty) } - entries.append(.message(message, peer, nil, nil, presentationData, 1, nil, false, .downloading(item.priority), resource, .downloading, allPaused)) + entries.append(.message(message, peer, nil, nil, presentationData, 1, nil, false, .downloading(item.priority), resource, .downloading, allPaused, nil)) } for item in downloadItems.doneItems.sorted(by: { ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $0.timestamp, index: $0.message.index) < ChatListSearchEntry.MessageOrderingKey.downloaded(timestamp: $1.timestamp, index: $1.message.index) }) { if !item.isSeen { @@ -1345,7 +1363,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - entries.append(.message(message, peer, nil, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false)) + entries.append(.message(message, peer, nil, nil, presentationData, 1, selectionState?.contains(message.id), false, .downloaded(timestamp: item.timestamp, index: message.index), (item.resourceId, item.size, false), .recentlyDownloaded, false, nil)) } return (entries.sorted(), false) } @@ -1875,7 +1893,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if lowercasedQuery.count > 1 && (presentationData.strings.DialogList_SavedMessages.lowercased().hasPrefix(lowercasedQuery) || "saved messages".hasPrefix(lowercasedQuery)) { if !existingPeerIds.contains(accountPeer.id), filteredPeer(EnginePeer(accountPeer), EnginePeer(accountPeer)) { existingPeerIds.insert(accountPeer.id) - entries.append(.localPeer(EnginePeer(accountPeer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + entries.append(.localPeer(EnginePeer(accountPeer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil)) index += 1 } } @@ -1893,7 +1911,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { associatedPeer = renderedPeer.peers[associatedPeerId] } - entries.append(.recentlySearchedPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder)) + entries.append(.recentlySearchedPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, nil)) index += 1 } @@ -1918,7 +1936,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if matches { existingPeerIds.insert(peer.id) - entries.append(.localPeer(peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + entries.append(.localPeer(peer, nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil)) } } } @@ -1941,7 +1959,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { associatedPeer = renderedPeer.peers[associatedPeerId] } - entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + entries.append(.localPeer(peer, associatedPeer, foundLocalPeers.unread[peer.id], index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil)) index += 1 numberOfLocalPeers += 1 } @@ -1955,7 +1973,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.localPeer(EnginePeer(peer.peer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType)) + entries.append(.localPeer(EnginePeer(peer.peer), nil, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, localExpandType, nil)) index += 1 numberOfLocalPeers += 1 } @@ -1972,7 +1990,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if !existingPeerIds.contains(peer.peer.id), filteredPeer(EnginePeer(peer.peer), EnginePeer(accountPeer)) { existingPeerIds.insert(peer.peer.id) - entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType)) + entries.append(.globalPeer(peer, nil, index, presentationData.theme, presentationData.strings, presentationData.nameSortOrder, presentationData.nameDisplayOrder, globalExpandType, nil)) index += 1 numberOfGlobalPeers += 1 } @@ -1986,7 +2004,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { peer = EngineRenderedPeer(peer: EnginePeer(channelPeer)) } } - entries.append(.message(message, peer, nil, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic, false)) + entries.append(.message(message, peer, nil, nil, presentationData, 1, nil, true, .index(message.index), nil, .generic, false, nil)) index += 1 } @@ -2017,7 +2035,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { } } - entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false)) + entries.append(.message(message, peer, foundRemoteMessageSet.readCounters[message.id.peerId], foundRemoteMessageSet.threadsData[message.id]?.info, presentationData, foundRemoteMessageSet.totalCount, selectionState?.contains(message.id), headerId == firstHeaderId, .index(message.index), nil, .generic, false, nil)) index += 1 } } @@ -2232,7 +2250,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var fetchResourceId: (id: String, size: Int64, isFirstInList: Bool)? for entry in currentEntries { switch entry { - case let .message(m, _, _, _, _, _, _, _, _, resource, _, _): + case let .message(m, _, _, _, _, _, _, _, _, resource, _, _, _): if m.id == message.id { fetchResourceId = resource } @@ -2298,7 +2316,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { if let entries = entriesAndFlags { var filteredEntries: [ChatListSearchEntry] = [] for entry in entries { - if case let .localPeer(peer, _, _, _, _, _, _, _, _) = entry { + if case let .localPeer(peer, _, _, _, _, _, _, _, _, _) = entry { peers.append(peer) } else if case .globalPeer = entry { } else { @@ -2411,7 +2429,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode { var messages: [EngineMessage] = [] for entry in newEntries { - if case let .message(message, _, _, _, _, _, _, _, _, _, _, _) = entry { + if case let .message(message, _, _, _, _, _, _, _, _, _, _, _, _) = entry { messages.append(message) } } diff --git a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift index be2a00a2c6..0923c3c18f 100644 --- a/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift +++ b/submodules/ChatListUI/Sources/ChatListSearchMediaNode.swift @@ -702,7 +702,7 @@ final class ChatListSearchMediaNode: ASDisplayNode, UIScrollViewDelegate { var index: UInt32 = 0 if let entries = entries { for entry in entries { - if case let .message(message, _, _, _, _, _, _, _, _, _, _, _) = entry { + if case let .message(message, _, _, _, _, _, _, _, _, _, _, _, _) = entry { self.mediaItems.append(VisualMediaItem(message: message._asMessage(), index: nil)) } index += 1 diff --git a/submodules/Display/Source/LinkHighlightingNode.swift b/submodules/Display/Source/LinkHighlightingNode.swift index 7d823e7e90..131875494c 100644 --- a/submodules/Display/Source/LinkHighlightingNode.swift +++ b/submodules/Display/Source/LinkHighlightingNode.swift @@ -79,7 +79,7 @@ public func generateRectsImage(color: UIColor, rects: [CGRect], inset: CGFloat, if useModernPathCalculation { if rects.count == 1 { - let path = UIBezierPath(roundedRect: rects[0], cornerRadius: outerRadius).cgPath + let path = UIBezierPath(roundedRect: rects[0].offsetBy(dx: -topLeft.x, dy: -topLeft.y), cornerRadius: outerRadius).cgPath context.addPath(path) if stroke { diff --git a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift index 3610eef1ad..960eacbdfd 100644 --- a/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift +++ b/submodules/HashtagSearchUI/Sources/HashtagSearchController.swift @@ -55,7 +55,7 @@ public final class HashtagSearchController: TelegramBaseController { |> map { result, presentationData in let result = result.0 let chatListPresentationData = ChatListPresentationData(theme: presentationData.theme, fontSize: presentationData.listsFontSize, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameSortOrder: presentationData.nameSortOrder, nameDisplayOrder: presentationData.nameDisplayOrder, disableAnimations: true) - return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap { EnginePeerReadCounters(state: $0, isMuted: false) }, nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false) }) + return result.messages.map({ .message(EngineMessage($0), EngineRenderedPeer(message: EngineMessage($0)), result.readStates[$0.id.peerId].flatMap { EnginePeerReadCounters(state: $0, isMuted: false) }, nil, chatListPresentationData, result.totalCount, nil, false, .index($0.index), nil, .generic, false, nil) }) } let interaction = ChatListNodeInteraction(context: context, animationCache: self.animationCache, animationRenderer: self.animationRenderer, activateSearch: { }, peerSelected: { _, _, _, _ in diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 5e932c4f5b..ef680b6bfd 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -2391,13 +2391,13 @@ public final class StandaloneReactionAnimation: ASDisplayNode { self.isUserInteractionEnabled = false } - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { - self.animateReactionSelection(context: context, theme: theme, animationCache: animationCache, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: @escaping () -> Void) { + self.animateReactionSelection(context: context, theme: theme, animationCache: animationCache, reaction: reaction, avatarPeers: avatarPeers, playHaptic: playHaptic, isLarge: isLarge, forceSmallEffectAnimation: forceSmallEffectAnimation, hideCenterAnimation: hideCenterAnimation, targetView: targetView, addStandaloneReactionAnimation: addStandaloneReactionAnimation, currentItemNode: nil, completion: completion) } public var currentDismissAnimation: (() -> Void)? - public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { + public func animateReactionSelection(context: AccountContext, theme: PresentationTheme, animationCache: AnimationCache, reaction: ReactionItem, avatarPeers: [EnginePeer], playHaptic: Bool, isLarge: Bool, forceSmallEffectAnimation: Bool = false, hideCenterAnimation: Bool = false, targetView: UIView, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, currentItemNode: ReactionNode?, completion: @escaping () -> Void) { guard let sourceSnapshotView = targetView.snapshotContentTree() else { completion() return @@ -2430,7 +2430,7 @@ public final class StandaloneReactionAnimation: ASDisplayNode { switchToInlineImmediately = false } - if !forceSmallEffectAnimation && !switchToInlineImmediately { + if !forceSmallEffectAnimation && !switchToInlineImmediately && !hideCenterAnimation { if let targetView = targetView as? ReactionIconView, !isLarge { self.itemNodeIsEmbedded = true targetView.addSubnode(itemNode) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 3ef4e1da57..95a74a4c93 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -137,6 +137,7 @@ public enum Stories { case isSelectedContacts case isForwardingDisabled case isEdited + case hasLike } public let id: Int32 @@ -156,6 +157,7 @@ public enum Stories { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool + public let hasLike: Bool public init( id: Int32, @@ -174,7 +176,8 @@ public enum Stories { isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, - isEdited: Bool + isEdited: Bool, + hasLike: Bool ) { self.id = id self.timestamp = timestamp @@ -193,6 +196,7 @@ public enum Stories { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited + self.hasLike = hasLike } public init(from decoder: Decoder) throws { @@ -221,6 +225,7 @@ public enum Stories { self.isSelectedContacts = try container.decodeIfPresent(Bool.self, forKey: .isSelectedContacts) ?? false self.isForwardingDisabled = try container.decodeIfPresent(Bool.self, forKey: .isForwardingDisabled) ?? false self.isEdited = try container.decodeIfPresent(Bool.self, forKey: .isEdited) ?? false + self.hasLike = try container.decodeIfPresent(Bool.self, forKey: .hasLike) ?? false } public func encode(to encoder: Encoder) throws { @@ -250,6 +255,7 @@ public enum Stories { try container.encode(self.isSelectedContacts, forKey: .isSelectedContacts) try container.encode(self.isForwardingDisabled, forKey: .isForwardingDisabled) try container.encode(self.isEdited, forKey: .isEdited) + try container.encode(self.hasLike, forKey: .hasLike) } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -311,6 +317,9 @@ public enum Stories { if lhs.isEdited != rhs.isEdited { return false } + if lhs.hasLike != rhs.hasLike { + return false + } return true } @@ -959,7 +968,8 @@ func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items.append(StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends)) @@ -1122,7 +1132,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { transaction.setStory(id: storyId, value: entry) @@ -1149,7 +1160,8 @@ func _internal_editStoryPrivacy(account: Account, id: Int32, privacy: EngineStor isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1276,7 +1288,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) if let entry = CodableEntry(Stories.StoredItem.item(updatedItem)) { items[index] = StoryItemsTableEntry(value: entry, id: item.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1302,7 +1315,8 @@ func _internal_updateStoriesArePinned(account: Account, ids: [Int32: EngineStory isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) updatedItems.append(updatedItem) } @@ -1420,7 +1434,8 @@ extension Stories.StoredItem { isContacts: isContacts, isSelectedContacts: isSelectedContacts, isForwardingDisabled: isForwardingDisabled, - isEdited: isEdited + isEdited: isEdited, + hasLike: false ) self = .item(item) } else { @@ -1585,15 +1600,18 @@ public final class EngineStoryViewListContext { public let peer: EnginePeer public let timestamp: Int32 public let storyStats: PeerStoryStats? + public let isLike: Bool public init( peer: EnginePeer, timestamp: Int32, - storyStats: PeerStoryStats? + storyStats: PeerStoryStats?, + isLike: Bool ) { self.peer = peer self.timestamp = timestamp self.storyStats = storyStats + self.isLike = isLike } public static func ==(lhs: Item, rhs: Item) -> Bool { @@ -1606,6 +1624,9 @@ public final class EngineStoryViewListContext { if lhs.storyStats != rhs.storyStats { return false } + if lhs.isLike != rhs.isLike { + return false + } return true } } @@ -1726,7 +1747,7 @@ public final class EngineStoryViewListContext { return previousData.withUpdatedIsBlocked(isBlocked).withUpdatedFlags(updatedFlags) }) if let peer = transaction.getPeer(peerId) { - items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId))) + items.append(Item(peer: EnginePeer(peer), timestamp: date, storyStats: transaction.getPeerStoryStats(peerId: peerId), isLike: false)) nextOffset = NextOffset(id: userId, timestamp: date) } @@ -1751,7 +1772,8 @@ public final class EngineStoryViewListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike )) if let entry = CodableEntry(updatedItem) { transaction.setStory(id: StoryId(peerId: account.peerId, id: storyId), value: entry) @@ -1779,7 +1801,8 @@ public final class EngineStoryViewListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1849,7 +1872,8 @@ public final class EngineStoryViewListContext { items[i] = Item( peer: item.peer, timestamp: item.timestamp, - storyStats: value + storyStats: value, + isLike: false ) } } @@ -2122,3 +2146,66 @@ public func _internal_setStoryNotificationWasDisplayed(transaction: Transaction, key.setInt32(8, value: id.id) transaction.putItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.displayedStoryNotifications, key: key), entry: CodableEntry(data: Data())) } + +func _internal_setStoryLike(account: Account, peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { + return account.postbox.transaction { transaction -> Void in + var currentItems = transaction.getStoryItems(peerId: peerId) + for i in 0 ..< currentItems.count { + if currentItems[i].id == id { + if case let .item(item) = currentItems[i].value.get(Stories.StoredItem.self) { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isEdited, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + hasLike: hasLike + )) + if let entry = CodableEntry(updatedItem) { + currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) + } + } + } + } + transaction.setStoryItems(peerId: peerId, items: currentItems) + + if let current = transaction.getStory(id: StoryId(peerId: peerId, id: id))?.get(Stories.StoredItem.self), case let .item(item) = current { + let updatedItem: Stories.StoredItem = .item(Stories.Item( + id: item.id, + timestamp: item.timestamp, + expirationTimestamp: item.expirationTimestamp, + media: item.media, + mediaAreas: item.mediaAreas, + text: item.text, + entities: item.entities, + views: item.views, + privacy: item.privacy, + isPinned: item.isPinned, + isExpired: item.isEdited, + isPublic: item.isPublic, + isCloseFriends: item.isCloseFriends, + isContacts: item.isContacts, + isSelectedContacts: item.isSelectedContacts, + isForwardingDisabled: item.isForwardingDisabled, + isEdited: item.isEdited, + hasLike: hasLike + )) + if let entry = CodableEntry(updatedItem) { + transaction.setStory(id: StoryId(peerId: peerId, id: id), value: entry) + } + } + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index d5582b3aef..c79f60e2de 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -49,8 +49,9 @@ public final class EngineStoryItem: Equatable { public let isSelectedContacts: Bool public let isForwardingDisabled: Bool public let isEdited: Bool + public let hasLike: Bool - public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool) { + public init(id: Int32, timestamp: Int32, expirationTimestamp: Int32, media: EngineMedia, mediaAreas: [MediaArea], text: String, entities: [MessageTextEntity], views: Views?, privacy: EngineStoryPrivacy?, isPinned: Bool, isExpired: Bool, isPublic: Bool, isPending: Bool, isCloseFriends: Bool, isContacts: Bool, isSelectedContacts: Bool, isForwardingDisabled: Bool, isEdited: Bool, hasLike: Bool) { self.id = id self.timestamp = timestamp self.expirationTimestamp = expirationTimestamp @@ -69,6 +70,7 @@ public final class EngineStoryItem: Equatable { self.isSelectedContacts = isSelectedContacts self.isForwardingDisabled = isForwardingDisabled self.isEdited = isEdited + self.hasLike = hasLike } public static func ==(lhs: EngineStoryItem, rhs: EngineStoryItem) -> Bool { @@ -126,6 +128,9 @@ public final class EngineStoryItem: Equatable { if lhs.isEdited != rhs.isEdited { return false } + if lhs.hasLike != rhs.hasLike { + return false + } return true } } @@ -159,7 +164,8 @@ extension EngineStoryItem { isContacts: self.isContacts, isSelectedContacts: self.isSelectedContacts, isForwardingDisabled: self.isForwardingDisabled, - isEdited: self.isEdited + isEdited: self.isEdited, + hasLike: self.hasLike ) } } @@ -528,7 +534,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) items.append(mappedItem) @@ -653,7 +660,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) storyItems.append(mappedItem) } @@ -802,7 +810,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) finalUpdatedState = updatedState } @@ -842,7 +851,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) finalUpdatedState = updatedState } else { @@ -884,7 +894,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -922,7 +933,8 @@ public final class PeerStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike )) updatedState.items.sort(by: { lhs, rhs in return lhs.timestamp > rhs.timestamp @@ -1084,7 +1096,8 @@ public final class PeerExpiringStoryListContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) items.append(.item(mappedItem)) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index f58ea6066a..628ee37757 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -1034,7 +1034,8 @@ public extension TelegramEngine { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike )) if let entry = CodableEntry(updatedItem) { currentItems[i] = StoryItemsTableEntry(value: entry, id: updatedItem.id, expirationTimestamp: updatedItem.expirationTimestamp, isCloseFriends: updatedItem.isCloseFriends) @@ -1109,5 +1110,9 @@ public extension TelegramEngine { public func enableStoryStealthMode() -> Signal { return _internal_enableStoryStealthMode(account: self.account) } + + public func setStoryLike(peerId: EnginePeer.Id, id: Int32, hasLike: Bool) -> Signal { + return _internal_setStoryLike(account: self.account, peerId: peerId, id: id, hasLike: hasLike) + } } } diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift index 188b00ec10..77f6f18dca 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourceKey.swift @@ -295,6 +295,8 @@ public enum PresentationResourceKey: Int32 { case chatGeneralThreadFreeIcon case uploadToneIcon + + case storyViewListLikeIcon } public enum ChatExpiredStoryIndicatorType: Hashable { diff --git a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift index 8f28cc78b7..c26414e8e6 100644 --- a/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift +++ b/submodules/TelegramPresentationData/Sources/Resources/PresentationResourcesChat.swift @@ -1298,4 +1298,10 @@ public struct PresentationResourcesChat { }) }) } + + public static func storyViewListLikeIcon(_ theme: PresentationTheme) -> UIImage? { + return theme.image(PresentationResourceKey.storyViewListLikeIcon.rawValue, { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Stories/InputLikeOn"), color: UIColor(rgb: 0xFF3B30)) + }) + } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 38a0d97d96..4e129a9d72 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1113,6 +1113,8 @@ final class MediaEditorScreenComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: nil, + hasLike: false, + likeAction: nil, inputModeAction: { [weak self] in if let self { switch self.currentInputMode { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 19d80e0e21..f590e981d2 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -266,6 +266,8 @@ final class StoryPreviewComponent: Component { stopAndPreviewMediaRecording: nil, discardMediaRecordingPreview: nil, attachmentAction: { }, + hasLike: false, + likeAction: nil, inputModeAction: nil, timeoutAction: nil, forwardAction: {}, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 2b848557c6..422ca44d16 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -19,6 +19,12 @@ private extension MessageInputActionButtonComponent.Mode { return "Chat/Input/Text/IconAttachment" case .forward: return "Chat/Input/Text/IconForwardSend" + case let .like(isActive): + if isActive { + return "Stories/InputLikeOn" + } else { + return "Stories/InputLikeOff" + } default: return nil } @@ -26,7 +32,7 @@ private extension MessageInputActionButtonComponent.Mode { } public final class MessageInputActionButtonComponent: Component { - public enum Mode { + public enum Mode: Equatable { case none case send case apply @@ -37,6 +43,7 @@ public final class MessageInputActionButtonComponent: Component { case attach case forward case more + case like(isActive: Bool) } public enum Action { @@ -299,7 +306,7 @@ public final class MessageInputActionButtonComponent: Component { switch component.mode { case .none: break - case .send, .apply, .attach, .delete, .forward: + case .send, .apply, .attach, .delete, .forward, .like: sendAlpha = 1.0 case .more: moreAlpha = 1.0 @@ -311,7 +318,11 @@ public final class MessageInputActionButtonComponent: Component { if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if let iconName = component.mode.iconName { - self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white) + var tintColor: UIColor = .white + if case .like(true) = component.mode { + tintColor = UIColor(rgb: 0xFF3B30) + } + self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: tintColor) } else if case .apply = component.mode { self.sendIconView.image = generateImage(CGSize(width: 33.0, height: 33.0), contextGenerator: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) @@ -407,7 +418,7 @@ public final class MessageInputActionButtonComponent: Component { if previousComponent?.mode != component.mode { switch component.mode { - case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more: + case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more, .like: micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate) case .videoInput: micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index ba05c0056f..0cf5e09875 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -79,6 +79,8 @@ public final class MessageInputPanelComponent: Component { public let stopAndPreviewMediaRecording: (() -> Void)? public let discardMediaRecordingPreview: (() -> Void)? public let attachmentAction: (() -> Void)? + public let hasLike: Bool + public let likeAction: (() -> Void)? public let inputModeAction: (() -> Void)? public let timeoutAction: ((UIView) -> Void)? public let forwardAction: (() -> Void)? @@ -124,6 +126,8 @@ public final class MessageInputPanelComponent: Component { stopAndPreviewMediaRecording: (() -> Void)?, discardMediaRecordingPreview: (() -> Void)?, attachmentAction: (() -> Void)?, + hasLike: Bool, + likeAction: (() -> Void)?, inputModeAction: (() -> Void)?, timeoutAction: ((UIView) -> Void)?, forwardAction: (() -> Void)?, @@ -168,6 +172,8 @@ public final class MessageInputPanelComponent: Component { self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording self.discardMediaRecordingPreview = discardMediaRecordingPreview self.attachmentAction = attachmentAction + self.hasLike = hasLike + self.likeAction = likeAction self.inputModeAction = inputModeAction self.timeoutAction = timeoutAction self.forwardAction = forwardAction @@ -271,6 +277,15 @@ public final class MessageInputPanelComponent: Component { if lhs.disabledPlaceholder != rhs.disabledPlaceholder { return false } + if (lhs.attachmentAction == nil) != (rhs.attachmentAction == nil) { + return false + } + if lhs.hasLike != rhs.hasLike { + return false + } + if (lhs.likeAction == nil) != (rhs.likeAction == nil) { + return false + } return true } @@ -296,6 +311,7 @@ public final class MessageInputPanelComponent: Component { private let attachmentButton = ComponentView() private var deleteMediaPreviewButton: ComponentView? private let inputActionButton = ComponentView() + private let likeButton = ComponentView() private let stickerButton = ComponentView() private let reactionButton = ComponentView() private let timeoutButton = ComponentView() @@ -325,6 +341,10 @@ public final class MessageInputPanelComponent: Component { private var component: MessageInputPanelComponent? private weak var state: EmptyComponentState? + public var likeButtonView: UIView? { + return self.likeButton.view + } + override init(frame: CGRect) { self.fieldBackgroundView = BlurredBackgroundView(color: UIColor(white: 0.0, alpha: 0.5), enableBlur: true) @@ -512,7 +532,7 @@ public final class MessageInputPanelComponent: Component { } func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 41.0) + var insets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0) if let _ = component.attachmentAction { insets.left = 41.0 @@ -521,10 +541,9 @@ public final class MessageInputPanelComponent: Component { insets.right = 41.0 } - let mediaInsets = UIEdgeInsets(top: insets.top, left: 7.0, bottom: insets.bottom, right: insets.right) + let mediaInsets = UIEdgeInsets(top: insets.top, left: 9.0, bottom: insets.bottom, right: 41.0) let baseFieldHeight: CGFloat = 40.0 - self.component = component self.state = state @@ -622,11 +641,16 @@ public final class MessageInputPanelComponent: Component { let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height)) - let fieldBackgroundFrame: CGRect + var fieldBackgroundFrame: CGRect if hasMediaRecording { fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - mediaInsets.right, height: textFieldSize.height)) - } else { + } else if isEditing { fieldBackgroundFrame = fieldFrame + } else { + fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height)) + if let _ = component.likeAction { + fieldBackgroundFrame.size.width -= 49.0 + } } transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size)) @@ -803,7 +827,7 @@ public final class MessageInputPanelComponent: Component { let attachmentButtonFrame = CGRect(origin: CGPoint(x: floor((insets.left - attachmentButtonSize.width) * 0.5) + (fieldBackgroundFrame.minX - fieldFrame.minX), y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - attachmentButtonSize.height) * 0.5)), size: attachmentButtonSize) transition.setPosition(view: attachmentButtonView, position: attachmentButtonFrame.center) transition.setBounds(view: attachmentButtonView, bounds: CGRect(origin: CGPoint(), size: attachmentButtonFrame.size)) - transition.setAlpha(view: attachmentButtonView, alpha: (hasMediaRecording || hasMediaEditing) ? 0.0 : 1.0) + transition.setAlpha(view: attachmentButtonView, alpha: (hasMediaRecording || hasMediaEditing || !isEditing) ? 0.0 : 1.0) transition.setScale(view: attachmentButtonView, scale: hasMediaEditing ? 0.001 : 1.0) } } @@ -994,20 +1018,72 @@ public final class MessageInputPanelComponent: Component { environment: {}, containerSize: CGSize(width: 33.0, height: 33.0) ) + + let hasLikeAction = !(isEditing || component.likeAction == nil) + + var inputActionButtonOriginX: CGFloat + if component.setMediaRecordingActive != nil || isEditing { + inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5) + } else { + inputActionButtonOriginX = size.width + } + + if hasLikeAction { + inputActionButtonOriginX += 3.0 + } + if let inputActionButtonView = self.inputActionButton.view { if inputActionButtonView.superview == nil { self.addSubview(inputActionButtonView) } - let inputActionButtonOriginX: CGFloat - if component.setMediaRecordingActive != nil || isEditing { - inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5) - } else { - inputActionButtonOriginX = size.width - } let inputActionButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - inputActionButtonSize.height) * 0.5)), size: inputActionButtonSize) transition.setPosition(view: inputActionButtonView, position: inputActionButtonFrame.center) transition.setBounds(view: inputActionButtonView, bounds: CGRect(origin: CGPoint(), size: inputActionButtonFrame.size)) - + inputActionButtonOriginX += 41.0 + } + + let likeButtonSize = self.likeButton.update( + transition: transition, + component: AnyComponent(MessageInputActionButtonComponent( + mode: .like(isActive: component.hasLike), + action: { [weak self] _, action, _ in + guard let self, let component = self.component else { + return + } + guard case .up = action else { + return + } + component.likeAction?() + }, + longPressAction: nil, + switchMediaInputMode: { + }, + updateMediaCancelFraction: { _ in + }, + lockMediaRecording: { + }, + stopAndPreviewMediaRecording: { + }, + moreAction: { _, _ in }, + context: component.context, + theme: component.theme, + strings: component.strings, + presentController: component.presentController, + audioRecorder: component.audioRecorder, + videoRecordingStatus: component.videoRecordingStatus + )), + environment: {}, + containerSize: CGSize(width: 33.0, height: 33.0) + ) + if let likeButtonView = self.likeButton.view { + if likeButtonView.superview == nil { + self.addSubview(likeButtonView) + } + let likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize) + transition.setPosition(view: likeButtonView, position: likeButtonFrame.center) + transition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size)) + transition.setAlpha(view: likeButtonView, alpha: hasLikeAction ? 1.0 : 0.0) + inputActionButtonOriginX += 41.0 } var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0 diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index 304cf81128..79be05eae0 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -327,7 +327,7 @@ final class PeerInfoStoryGridScreenComponent: Component { let buttonText: String switch component.scope { case .saved: - buttonText = environment.strings.Common_Delete + buttonText = environment.strings.ChatList_Context_Archive case .archive: buttonText = environment.strings.StoryList_SaveToProfile } @@ -350,31 +350,22 @@ final class PeerInfoStoryGridScreenComponent: Component { switch component.scope { case .saved: - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) - let actionSheet = ActionSheetController(presentationData: presentationData) + let selectedCount = paneNode.selectedItems.count + let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: false).start() - actionSheet.setItemGroups([ - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in - actionSheet?.dismissAnimated() - - guard let self, let paneNode = self.paneNode, let component = self.component else { - return - } - let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start() - - paneNode.setIsSelectionModeActive(false) - (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() - }) - ]), - ActionSheetItemGroup(items: [ - ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in - actionSheet?.dismissAnimated() - }) - ]) - ]) + paneNode.setIsSelectionModeActive(false) + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() - self.environment?.controller()?.present(actionSheet, in: .window(.root)) + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(selectedCount)) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: nil, text: title, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) case .archive: let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start() @@ -591,7 +582,9 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { return } let title: String? - if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty { + if componentView.selectedCount != 0 { + title = presentationData.strings.StoryList_SubtitleSelected(Int32(componentView.selectedCount)) + } else if let paneStatusText = componentView.paneStatusText, !paneStatusText.isEmpty { title = paneStatusText } else { title = nil diff --git a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift index d8ab22121d..5fe7060678 100644 --- a/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift +++ b/submodules/TelegramUI/Components/Stories/PeerListItemComponent/Sources/PeerListItemComponent.swift @@ -55,6 +55,7 @@ public final class PeerListItemComponent: Component { let subtitle: String? let subtitleAccessory: SubtitleAccessory let presence: EnginePeer.Presence? + let displayLike: Bool let selectionState: SelectionState let hasNext: Bool let action: (EnginePeer) -> Void @@ -73,6 +74,7 @@ public final class PeerListItemComponent: Component { subtitle: String?, subtitleAccessory: SubtitleAccessory, presence: EnginePeer.Presence?, + displayLike: Bool = false, selectionState: SelectionState, hasNext: Bool, action: @escaping (EnginePeer) -> Void, @@ -90,6 +92,7 @@ public final class PeerListItemComponent: Component { self.subtitle = subtitle self.subtitleAccessory = subtitleAccessory self.presence = presence + self.displayLike = displayLike self.selectionState = selectionState self.hasNext = hasNext self.action = action @@ -131,6 +134,9 @@ public final class PeerListItemComponent: Component { if lhs.presence != rhs.presence { return false } + if lhs.displayLike != rhs.displayLike { + return false + } if lhs.selectionState != rhs.selectionState { return false } @@ -154,6 +160,8 @@ public final class PeerListItemComponent: Component { private var iconView: UIImageView? private var checkLayer: CheckLayer? + private var likeIconView: UIImageView? + private var component: PeerListItemComponent? private weak var state: EmptyComponentState? @@ -340,7 +348,11 @@ public final class PeerListItemComponent: Component { if case .generic = component.style { leftInset += 9.0 } - let rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset + if component.displayLike { + rightInset += 32.0 + } + var avatarLeftInset: CGFloat = component.sideInset + 10.0 if case let .editing(isSelected, isTinted) = component.selectionState { @@ -567,6 +579,27 @@ public final class PeerListItemComponent: Component { transition.setFrame(view: labelView, frame: labelFrame) } + if component.displayLike { + let likeIconView: UIImageView + if let current = self.likeIconView { + likeIconView = current + } else { + likeIconView = UIImageView() + self.likeIconView = likeIconView + self.containerButton.addSubview(likeIconView) + + likeIconView.image = PresentationResourcesChat.storyViewListLikeIcon(component.theme) + } + + if let _ = likeIconView.image { + let imageSize = CGSize(width: 32.0, height: 32.0) + transition.setFrame(view: likeIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - (contextInset * 2.0 + 11.0 + component.sideInset) - imageSize.width, y: floor((height - verticalInset * 2.0 - imageSize.height) * 0.5)), size: imageSize)) + } + } else if let likeIconView = self.likeIconView { + self.likeIconView = nil + likeIconView.removeFromSuperview() + } + if themeUpdated { self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 9e57d0a0a0..38276c86aa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -172,7 +172,8 @@ public final class StoryContentContextImpl: StoryContentContext { isContacts: item.isContacts, isSelectedContacts: item.isSelectedContacts, isForwardingDisabled: item.isForwardingDisabled, - isEdited: item.isEdited + isEdited: item.isEdited, + hasLike: item.hasLike ) } var totalCount = peerStoryItemsView.items.count @@ -196,7 +197,8 @@ public final class StoryContentContextImpl: StoryContentContext { isContacts: item.privacy.base == .contacts, isSelectedContacts: item.privacy.base == .nobody, isForwardingDisabled: false, - isEdited: false + isEdited: false, + hasLike: false )) totalCount += 1 } @@ -1041,7 +1043,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { isContacts: itemValue.isContacts, isSelectedContacts: itemValue.isSelectedContacts, isForwardingDisabled: itemValue.isForwardingDisabled, - isEdited: itemValue.isEdited + isEdited: itemValue.isEdited, + hasLike: itemValue.hasLike ) let mainItem = StoryContentItem( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift index 08ee615ac4..bd77d6434a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContentCaptionComponent.swift @@ -151,6 +151,8 @@ final class StoryContentCaptionComponent: Component { private let collapsedText: ContentItem private let expandedText: ContentItem private var textSelectionNode: TextSelectionNode? + private let textSelectionKnobContainer: UIView + private var textSelectionKnobSurface: UIView? private let scrollMaskContainer: UIView private let scrollFullMaskView: UIView @@ -219,6 +221,9 @@ final class StoryContentCaptionComponent: Component { self.collapsedText = ContentItem(frame: CGRect()) self.expandedText = ContentItem(frame: CGRect()) + + self.textSelectionKnobContainer = UIView() + self.textSelectionKnobContainer.isUserInteractionEnabled = false super.init(frame: frame) @@ -232,6 +237,8 @@ final class StoryContentCaptionComponent: Component { self.scrollView.addSubview(self.expandedText) self.scrollViewContainer.mask = self.scrollMaskContainer + + self.addSubview(self.textSelectionKnobContainer) } required init?(coder: NSCoder) { @@ -335,6 +342,8 @@ final class StoryContentCaptionComponent: Component { let edgeDistanceFraction = edgeDistance / 7.0 transition.setAlpha(view: self.scrollFullMaskView, alpha: 1.0 - edgeDistanceFraction) + transition.setBounds(view: self.textSelectionKnobContainer, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.scrollView.bounds.minY), size: CGSize())) + let shadowOverflow: CGFloat = 58.0 let shadowFrame = CGRect(origin: CGPoint(x: 0.0, y: -self.scrollView.contentOffset.y + itemLayout.containerSize.height - itemLayout.visibleTextHeight - itemLayout.verticalInset - shadowOverflow), size: CGSize(width: itemLayout.containerSize.width, height: itemLayout.visibleTextHeight + itemLayout.verticalInset + shadowOverflow)) @@ -702,6 +711,13 @@ final class StoryContentCaptionComponent: Component { if self.textSelectionNode == nil, let controller = component.controller(), let textNode = self.expandedText.textNode?.textNode { let selectionColor = UIColor(white: 1.0, alpha: 0.5) + + if self.textSelectionKnobSurface == nil { + let textSelectionKnobSurface = UIView() + self.textSelectionKnobSurface = textSelectionKnobSurface + self.textSelectionKnobContainer.addSubview(textSelectionKnobSurface) + } + let textSelectionNode = TextSelectionNode(theme: TextSelectionTheme(selection: selectionColor, knob: component.theme.list.itemAccentColor), strings: component.strings, textNode: textNode, updateIsActive: { [weak self] value in guard let self else { return @@ -718,7 +734,7 @@ final class StoryContentCaptionComponent: Component { return } component.controller()?.presentInGlobalOverlay(c, with: a) - }, rootNode: controller.displayNode, performAction: { [weak self] text, action in + }, rootNode: controller.displayNode, externalKnobSurface: self.textSelectionKnobSurface, performAction: { [weak self] text, action in guard let self, let component = self.component else { return } @@ -806,6 +822,9 @@ final class StoryContentCaptionComponent: Component { if let textSelectionNode = self.textSelectionNode, let textNode = self.expandedText.textNode?.textNode { textSelectionNode.frame = textNode.frame.offsetBy(dx: self.expandedText.frame.minX, dy: self.expandedText.frame.minY) textSelectionNode.highlightAreaNode.frame = textSelectionNode.frame + if let textSelectionKnobSurface = self.textSelectionKnobSurface { + textSelectionKnobSurface.frame = textSelectionNode.frame + } } self.itemLayout = ItemLayout( diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index c41f20aa8b..ee2c33815c 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -196,6 +196,9 @@ public final class StoryItemSetContainerComponent: Component { if lhs.context !== rhs.context { return false } + if lhs.availableReactions !== rhs.availableReactions { + return false + } if lhs.slice != rhs.slice { return false } @@ -288,6 +291,7 @@ public final class StoryItemSetContainerComponent: Component { final class VisibleItem { let externalState = StoryContentItem.ExternalState() + let unclippedContainerView: UIView let contentContainerView: UIView let contentTintLayer = SimpleLayer() let view = ComponentView() @@ -296,6 +300,9 @@ public final class StoryItemSetContainerComponent: Component { var requestedNext: Bool = false init() { + self.unclippedContainerView = UIView() + self.unclippedContainerView.isUserInteractionEnabled = false + self.contentContainerView = UIView() self.contentContainerView.clipsToBounds = true if #available(iOS 13.0, *) { @@ -370,6 +377,7 @@ public final class StoryItemSetContainerComponent: Component { let itemsContainerView: UIView let controlsContainerView: UIView + let controlsClippingView: UIView let topContentGradientView: UIImageView let bottomContentGradientLayer: SimpleGradientLayer let contentDimView: UIView @@ -453,9 +461,10 @@ public final class StoryItemSetContainerComponent: Component { self.scroller.delaysContentTouches = false self.controlsContainerView = SparseContainerView() - self.controlsContainerView.clipsToBounds = true + self.controlsClippingView = SparseContainerView() + self.controlsClippingView.clipsToBounds = true if #available(iOS 13.0, *) { - self.controlsContainerView.layer.cornerCurve = .continuous + self.controlsClippingView.layer.cornerCurve = .continuous } self.topContentGradientView = UIImageView() @@ -486,14 +495,15 @@ public final class StoryItemSetContainerComponent: Component { self.itemsContainerView.addGestureRecognizer(self.scroller.panGestureRecognizer) self.addSubview(self.itemsContainerView) + self.addSubview(self.controlsClippingView) self.addSubview(self.controlsContainerView) - self.controlsContainerView.addSubview(self.contentDimView) - self.controlsContainerView.addSubview(self.topContentGradientView) + self.controlsClippingView.addSubview(self.contentDimView) + self.controlsClippingView.addSubview(self.topContentGradientView) self.layer.addSublayer(self.bottomContentGradientLayer) self.closeButton.addSubview(self.closeButtonIconView) - self.controlsContainerView.addSubview(self.closeButton) + self.controlsClippingView.addSubview(self.closeButton) self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) self.addSubview(self.viewListsContainer) @@ -661,6 +671,15 @@ public final class StoryItemSetContainerComponent: Component { } } + if self.controlsClippingView.frame.contains(point) { + if let result = self.controlsClippingView.hitTest(self.convert(point, to: self.controlsClippingView), with: nil) { + if result != self.controlsClippingView { + return false + } + } + return true + } + if self.controlsContainerView.frame.contains(point) { if let result = self.controlsContainerView.hitTest(self.convert(point, to: self.controlsContainerView), with: nil) { if result != self.controlsContainerView { @@ -932,9 +951,11 @@ public final class StoryItemSetContainerComponent: Component { guard let result = super.hitTest(point, with: event) else { return nil } + if result === self.scroller { return self.itemsContainerView } + return result } @@ -1202,6 +1223,7 @@ public final class StoryItemSetContainerComponent: Component { if visibleItem.contentContainerView.superview == nil { self.itemsContainerView.addSubview(visibleItem.contentContainerView) self.itemsContainerView.layer.addSublayer(visibleItem.contentTintLayer) + self.itemsContainerView.addSubview(visibleItem.unclippedContainerView) visibleItem.contentContainerView.addSubview(view) } @@ -1216,10 +1238,14 @@ public final class StoryItemSetContainerComponent: Component { if !self.trulyValidIds.contains(itemId), let visibleItem = self.visibleItems[itemId] { self.visibleItems.removeValue(forKey: itemId) visibleItem.contentContainerView.removeFromSuperview() + visibleItem.unclippedContainerView.removeFromSuperview() } }) itemTransition.setBounds(view: visibleItem.contentContainerView, bounds: CGRect(origin: CGPoint(), size: itemLayout.contentFrame.size)) + itemTransition.setPosition(view: visibleItem.unclippedContainerView, position: CGPoint(x: itemPositionX, y: itemLayout.contentFrame.center.y)) + itemTransition.setBounds(view: visibleItem.unclippedContainerView, 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)) @@ -1240,6 +1266,8 @@ public final class StoryItemSetContainerComponent: Component { itemTransition.setTransform(view: visibleItem.contentContainerView, transform: transform) itemTransition.setCornerRadius(layer: visibleItem.contentContainerView.layer, cornerRadius: 12.0 * (1.0 / itemScale)) + itemTransition.setTransform(view: visibleItem.unclippedContainerView, transform: transform) + itemTransition.setTransform(layer: visibleItem.contentTintLayer, transform: transform) let countedFractionDistanceToCenter: CGFloat = max(0.0, min(1.0, unboundFractionDistanceToCenter / 3.0)) @@ -1270,6 +1298,7 @@ public final class StoryItemSetContainerComponent: Component { if !validIds.contains(id) { removeIds.append(id) visibleItem.contentContainerView.removeFromSuperview() + visibleItem.unclippedContainerView.removeFromSuperview() visibleItem.contentTintLayer.removeFromSuperlayer() } } @@ -1390,7 +1419,10 @@ public final class StoryItemSetContainerComponent: Component { captionItemView.layer.animateAlpha(from: 0.0, to: captionItemView.alpha, duration: 0.28) } - if let component = self.component, let sourceView = transitionIn.sourceView, let contentContainerView = self.visibleItems[component.slice.item.storyItem.id]?.contentContainerView { + if let component = self.component, let sourceView = transitionIn.sourceView, let visibleItem = self.visibleItems[component.slice.item.storyItem.id] { + let contentContainerView = visibleItem.contentContainerView + let unclippedContainerView = visibleItem.unclippedContainerView + if let centerInfoView = self.centerInfoItem?.view.view { centerInfoView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } @@ -1431,11 +1463,17 @@ public final class StoryItemSetContainerComponent: Component { duration: 0.3 ) + unclippedContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: contentContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + unclippedContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: contentContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.controlsContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsContainerView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) self.controlsContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsContainerView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) - self.controlsContainerView.layer.animate( + + self.controlsClippingView.layer.animatePosition(from: sourceLocalFrame.center, to: self.controlsClippingView.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.controlsClippingView.layer.animateBounds(from: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), to: self.controlsClippingView.bounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) + self.controlsClippingView.layer.animate( from: transitionIn.sourceCornerRadius as NSNumber, - to: self.controlsContainerView.layer.cornerRadius as NSNumber, + to: self.controlsClippingView.layer.cornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.3 @@ -1532,7 +1570,10 @@ public final class StoryItemSetContainerComponent: Component { }) } - if let component = self.component, let sourceView = transitionOut.destinationView, let contentContainerView = self.visibleItems[component.slice.item.storyItem.id]?.contentContainerView { + if let component = self.component, let sourceView = transitionOut.destinationView, let visibleItem = self.visibleItems[component.slice.item.storyItem.id] { + let contentContainerView = visibleItem.contentContainerView + let unclippedContainerView = visibleItem.unclippedContainerView + let sourceLocalFrame = sourceView.convert(transitionOut.destinationRect, to: self) let innerSourceLocalFrame = CGRect(origin: CGPoint(x: sourceLocalFrame.minX - contentContainerView.frame.minX, y: sourceLocalFrame.minY - contentContainerView.frame.minY), size: sourceLocalFrame.size) @@ -1614,7 +1655,9 @@ public final class StoryItemSetContainerComponent: Component { } contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) for transitionViewImpl in transitionViewsImpl { transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) @@ -1652,10 +1695,16 @@ public final class StoryItemSetContainerComponent: Component { removeOnCompletion: false ) + unclippedContainerView.layer.animatePosition(from: contentContainerView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + unclippedContainerView.layer.animateBounds(from: contentContainerView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.controlsContainerView.layer.animatePosition(from: self.controlsContainerView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) self.controlsContainerView.layer.animateBounds(from: self.controlsContainerView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.controlsContainerView.layer.animate( - from: self.controlsContainerView.layer.cornerRadius as NSNumber, + + self.controlsClippingView.layer.animatePosition(from: self.controlsClippingView.center, to: sourceLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.controlsClippingView.layer.animateBounds(from: self.controlsClippingView.bounds, to: CGRect(origin: CGPoint(x: innerSourceLocalFrame.minX, y: innerSourceLocalFrame.minY), size: sourceLocalFrame.size), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.controlsClippingView.layer.animate( + from: self.controlsClippingView.layer.cornerRadius as NSNumber, to: transitionOut.destinationCornerRadius as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, @@ -1716,7 +1765,9 @@ public final class StoryItemSetContainerComponent: Component { transitionViewImpl.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1) } contentContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + unclippedContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) self.controlsContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.controlsClippingView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) for transitionViewImpl in transitionViewsImpl { transition.setFrame(view: transitionViewImpl, frame: sourceLocalFrame) @@ -1989,6 +2040,13 @@ public final class StoryItemSetContainerComponent: Component { } self.sendMessageContext.presentAttachmentMenu(view: self, subject: .default) }, + hasLike: component.slice.item.storyItem.hasLike, + likeAction: component.slice.peer.isService ? nil : { [weak self] in + guard let self else { + return + } + self.performLikeAction() + }, inputModeAction: { [weak self] in guard let self else { return @@ -2378,12 +2436,13 @@ public final class StoryItemSetContainerComponent: Component { } if isContact { + //TODO:localize itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in f(.default) - let _ = component.context.engine.contacts.deleteContactPeerInteractively(peerId: peer.id) + let _ = component.context.engine.contacts.deleteContactPeerInteractively(peerId: peer.id).start() guard let self else { return @@ -2568,6 +2627,9 @@ public final class StoryItemSetContainerComponent: Component { transition.setPosition(view: self.controlsContainerView, position: contentFrame.center) transition.setBounds(view: self.controlsContainerView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) + transition.setPosition(view: self.controlsClippingView, position: contentFrame.center) + transition.setBounds(view: self.controlsClippingView, bounds: CGRect(origin: CGPoint(), size: contentFrame.size)) + var transform = CATransform3DMakeScale(contentVisualScale, contentVisualScale, 1.0) if let pinchState = component.pinchState { let pinchOffset = CGPoint( @@ -2583,8 +2645,9 @@ public final class StoryItemSetContainerComponent: Component { transform = CATransform3DScale(transform, pinchState.scale, pinchState.scale, 0.0) } transition.setTransform(view: self.controlsContainerView, transform: transform) + transition.setTransform(view: self.controlsClippingView, transform: transform) - transition.setCornerRadius(layer: self.controlsContainerView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale)) + transition.setCornerRadius(layer: self.controlsClippingView.layer, cornerRadius: 12.0 * (1.0 / contentVisualScale)) var headerRightOffset: CGFloat = availableSize.width @@ -2630,7 +2693,7 @@ public final class StoryItemSetContainerComponent: Component { ) if let moreButtonView = self.moreButton.view { if moreButtonView.superview == nil { - self.controlsContainerView.addSubview(moreButtonView) + self.controlsClippingView.addSubview(moreButtonView) } moreButtonView.isUserInteractionEnabled = !component.slice.item.storyItem.isPending transition.setFrame(view: moreButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - moreButtonSize.width, y: 2.0), size: moreButtonSize)) @@ -2697,7 +2760,7 @@ public final class StoryItemSetContainerComponent: Component { if let soundButtonView = self.soundButton.view { if soundButtonView.superview == nil { - self.controlsContainerView.addSubview(soundButtonView) + self.controlsClippingView.addSubview(soundButtonView) } transition.setFrame(view: soundButtonView, frame: CGRect(origin: CGPoint(x: headerRightOffset - soundButtonSize.width, y: 2.0), size: soundButtonSize)) transition.setAlpha(view: soundButtonView, alpha: soundAlpha) @@ -2799,7 +2862,7 @@ public final class StoryItemSetContainerComponent: Component { let closeFriendIconFrame = CGRect(origin: CGPoint(x: headerRightOffset - privacyIconSize.width - 8.0, y: 22.0), size: privacyIconSize) if let closeFriendIconView = privacyIcon.view { if closeFriendIconView.superview == nil { - self.controlsContainerView.addSubview(closeFriendIconView) + self.controlsClippingView.addSubview(closeFriendIconView) } privacyIconTransition.setFrame(view: closeFriendIconView, frame: closeFriendIconFrame) @@ -2810,7 +2873,9 @@ public final class StoryItemSetContainerComponent: Component { closeFriendIcon.view?.removeFromSuperview() } - transition.setAlpha(view: self.controlsContainerView, alpha: (component.hideUI || self.isEditingStory || self.displayViewList) ? 0.0 : 1.0) + let controlsContainerAlpha = (component.hideUI || self.isEditingStory || self.displayViewList) ? 0.0 : 1.0 + transition.setAlpha(view: self.controlsContainerView, alpha: controlsContainerAlpha) + transition.setAlpha(view: self.controlsClippingView, alpha: controlsContainerAlpha) let focusedItem: StoryContentItem? = component.slice.item @@ -2887,7 +2952,7 @@ public final class StoryItemSetContainerComponent: Component { if let view = currentCenterInfoItem.view.view { var animateIn = false if view.superview == nil { - self.controlsContainerView.insertSubview(view, belowSubview: self.closeButton) + self.controlsClippingView.insertSubview(view, belowSubview: self.closeButton) animateIn = true } transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 0.0, y: 10.0), size: centerInfoItemSize)) @@ -2921,7 +2986,7 @@ public final class StoryItemSetContainerComponent: Component { if let view = currentLeftInfoItem.view.view { var animateIn = false if view.superview == nil { - self.controlsContainerView.addSubview(view) + self.controlsClippingView.addSubview(view) animateIn = true } transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: 12.0, y: 18.0), size: leftInfoItemSize)) @@ -3469,7 +3534,7 @@ public final class StoryItemSetContainerComponent: Component { if let navigationStripView = self.navigationStrip.view { if navigationStripView.superview == nil { navigationStripView.isUserInteractionEnabled = false - self.controlsContainerView.addSubview(navigationStripView) + self.controlsClippingView.addSubview(navigationStripView) } transition.setFrame(view: navigationStripView, frame: CGRect(origin: CGPoint(x: navigationStripSideInset, y: navigationStripTopInset), size: CGSize(width: availableSize.width - navigationStripSideInset * 2.0, height: 2.0))) transition.setAlpha(view: navigationStripView, alpha: self.isEditingStory ? 0.0 : 1.0) @@ -3997,6 +4062,65 @@ public final class StoryItemSetContainerComponent: Component { } } + private func performLikeAction() { + guard let component = self.component else { + return + } + guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else { + return + } + guard let likeButtonView = inputPanelView.likeButtonView else { + return + } + + let _ = component.context.engine.messages.setStoryLike(peerId: component.slice.peer.id, id: component.slice.item.storyItem.id, hasLike: !component.slice.item.storyItem.hasLike).start() + + if component.slice.item.storyItem.hasLike { + return + } + + var reactionItem: ReactionItem? + guard let availableReactions = component.availableReactions else { + return + } + for item in availableReactions.reactionItems { + if case .builtin("❤") = item.reaction.rawValue { + reactionItem = item + break + } + } + + guard let reactionItem else { + return + } + + let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) + self.addSubnode(standaloneReactionAnimation) + standaloneReactionAnimation.frame = self.bounds + standaloneReactionAnimation.animateReactionSelection( + context: component.context, + theme: component.theme, + animationCache: component.context.animationCache, + reaction: reactionItem, + avatarPeers: [], + playHaptic: true, + isLarge: false, + hideCenterAnimation: true, + targetView: likeButtonView, + addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + guard let self else { + return + } + + standaloneReactionAnimation.frame = self.bounds + self.addSubnode(standaloneReactionAnimation) + }, + completion: { [weak standaloneReactionAnimation] in + standaloneReactionAnimation?.removeFromSupernode() + } + ) + } + func dismissAllTooltips() { guard let component = self.component, let controller = component.controller() else { return diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 339e80b4c9..b32e58ebce 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -517,6 +517,7 @@ final class StoryItemSetViewListComponent: Component { subtitle: dateText, subtitleAccessory: .checks, presence: nil, + displayLike: item.isLike, selectionState: .none, hasNext: index != viewListState.totalCount - 1, action: { [weak self] peer in diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 1683305c01..f7ff5b930a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -297,7 +297,7 @@ public final class StoryFooterPanelComponent: Component { if viewCount != 0 { regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white))) } - regularSegments.append(.text(1, NSAttributedString(string: " Views", font: Font.regular(15.0), textColor: .white))) + regularSegments.append(.text(1, NSAttributedString(string: viewCount == 1 ? " View" : " Views", font: Font.regular(15.0), textColor: .white))) var expandedSegments: [AnimatedCountLabelView.Segment] = [] if viewCount != 0 { diff --git a/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/Contents.json new file mode 100644 index 0000000000..78d19dd9e0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "like_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/like_30.pdf b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/like_30.pdf new file mode 100644 index 0000000000..8a3fc417c3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOff.imageset/like_30.pdf @@ -0,0 +1,135 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.375000 3.032455 cm +0.000000 0.000000 0.000000 scn +11.603342 2.059748 m +12.047447 1.358528 l +12.052738 1.361937 l +11.603342 2.059748 l +h +10.625000 18.449600 m +9.893637 18.057159 l +10.037730 17.788624 10.317400 17.620649 10.622152 17.619604 c +10.926906 17.618559 11.207721 17.784609 11.353653 18.052153 c +10.625000 18.449600 l +h +9.646659 2.059748 m +9.197262 1.361937 l +9.202449 1.358595 9.207672 1.355312 9.212932 1.352089 c +9.646659 2.059748 l +h +10.625000 0.829996 m +10.934997 0.829996 11.227332 0.935177 11.431684 1.024776 c +11.653996 1.122252 11.868692 1.245344 12.047435 1.358549 c +11.159248 2.760948 l +11.022397 2.674276 10.884680 2.597492 10.765099 2.545061 c +10.706002 2.519150 10.660861 2.503416 10.629790 2.494913 c +10.596159 2.485710 10.597184 2.489996 10.625000 2.489996 c +10.625000 0.829996 l +h +12.052738 1.361937 m +17.971605 5.173729 22.080000 9.829360 22.080000 14.788708 c +20.420000 14.788708 l +20.420000 10.743106 16.996216 6.520025 11.153945 2.757561 c +12.052738 1.361937 l +h +22.080000 14.788708 m +22.080000 19.057514 19.095673 22.172544 15.232674 22.172544 c +15.232674 20.512545 l +18.081306 20.512545 20.420000 18.241436 20.420000 14.788708 c +22.080000 14.788708 l +h +15.232674 22.172544 m +12.780071 22.172544 10.959950 20.796986 9.896347 18.847046 c +11.353653 18.052153 l +12.183614 19.573746 13.498394 20.512545 15.232674 20.512545 c +15.232674 22.172544 l +h +11.356363 18.842037 m +10.312891 20.786690 8.479945 22.172544 6.027846 22.172544 c +6.027846 20.512545 l +7.762629 20.512545 9.085624 19.563004 9.893637 18.057159 c +11.356363 18.842037 l +h +6.027846 22.172544 m +2.166678 22.172544 -0.830000 19.059464 -0.830000 14.788708 c +0.830000 14.788708 l +0.830000 18.239487 3.177382 20.512545 6.027846 20.512545 c +6.027846 22.172544 l +h +-0.830000 14.788708 m +-0.830000 9.829360 3.278394 5.173729 9.197262 1.361937 c +10.096055 2.757561 l +4.253784 6.520025 0.830000 10.743106 0.830000 14.788708 c +-0.830000 14.788708 l +h +9.212932 1.352089 m +9.392480 1.242044 9.607049 1.120869 9.826206 1.024776 c +10.024843 0.937681 10.316897 0.829996 10.625000 0.829996 c +10.625000 2.489996 l +10.658530 2.489996 10.663249 2.484592 10.629533 2.493975 c +10.598954 2.502481 10.553483 2.518450 10.492792 2.545061 c +10.370055 2.598877 10.226951 2.677576 10.080385 2.767408 c +9.212932 1.352089 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 2357 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002447 00000 n +0000002470 00000 n +0000002643 00000 n +0000002717 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2776 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/Contents.json new file mode 100644 index 0000000000..77fea60253 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "heartfilled_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/heartfilled_30.pdf b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/heartfilled_30.pdf new file mode 100644 index 0000000000..cca1397b18 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Stories/InputLikeOn.imageset/heartfilled_30.pdf @@ -0,0 +1,75 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 4.375000 4.692444 cm +0.000000 0.000000 0.000000 scn +10.625000 0.000008 m +10.898515 0.000008 11.287747 0.199884 11.603342 0.399759 c +17.483912 4.186889 21.250000 8.626245 21.250000 13.128719 c +21.250000 16.989487 18.588490 19.682556 15.232674 19.682556 c +13.139233 19.682556 11.571782 18.525377 10.625000 16.789612 c +9.699258 18.514858 8.121287 19.682556 6.027846 19.682556 c +2.672030 19.682556 0.000000 16.989487 0.000000 13.128719 c +0.000000 8.626245 3.766089 4.186889 9.646659 0.399759 c +9.972773 0.199884 10.362005 0.000008 10.625000 0.000008 c +h +f +n +Q + +endstream +endobj + +3 0 obj + 623 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000713 00000 n +0000000735 00000 n +0000000908 00000 n +0000000982 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1041 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift index c5e210744b..28a40df703 100644 --- a/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageBubbleItemNode.swift @@ -103,6 +103,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ var skipText = false var messageWithCaptionToAdd: (Message, ChatMessageEntryAttributes)? var isUnsupportedMedia = false + var isStoryWithText = false var isAction = false var previousItemIsFile = false @@ -140,6 +141,12 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ } else { result.append((message, ChatMessageMediaBubbleContentNode.self, itemAttributes, BubbleItemAttributes(isAttachment: false, neighborType: .media, neighborSpacing: .default))) } + + if let storyItem = message.associatedStories[story.storyId], let storedItem = storyItem.get(Stories.StoredItem.self), case let .item(item) = storedItem { + if !item.text.isEmpty { + isStoryWithText = true + } + } } } else if let file = media as? TelegramMediaFile { let isVideo = file.isVideo || (file.isAnimated && file.dimensions != nil) @@ -216,7 +223,7 @@ private func contentNodeMessagesAndClassesForItem(_ item: ChatMessageItem) -> ([ messageText = updatingMedia.text } - if !messageText.isEmpty || isUnsupportedMedia { + if !messageText.isEmpty || isUnsupportedMedia || isStoryWithText { if !skipText { if case .group = item.content, !isFile { messageWithCaptionToAdd = (message, itemAttributes) diff --git a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift index 39a1affa5f..ecfbafbe87 100644 --- a/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageTextBubbleContentNode.swift @@ -218,6 +218,7 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { var mediaDuration: Double? = nil var isSeekableWebMedia = false var isUnsupportedMedia = false + var story: Stories.Item? for media in item.message.media { if let file = media as? TelegramMediaFile, let duration = file.duration { mediaDuration = Double(duration) @@ -226,11 +227,20 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { isSeekableWebMedia = true } else if media is TelegramMediaUnsupported { isUnsupportedMedia = true + } else if let storyMedia = media as? TelegramMediaStory { + if let value = item.message.associatedStories[storyMedia.storyId]?.get(Stories.StoredItem.self) { + if case let .item(storyValue) = value { + story = storyValue + } + } } } var isTranslating = false - if isUnsupportedMedia { + if let story { + rawText = story.text + messageEntities = story.entities + } else if isUnsupportedMedia { rawText = item.presentationData.strings.Conversation_UnsupportedMediaPlaceholder messageEntities = [MessageTextEntity(range: 0.. Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) { + public init(theme: TextSelectionTheme, strings: PresentationStrings, textNode: TextNode, updateIsActive: @escaping (Bool) -> Void, present: @escaping (ViewController, Any?) -> Void, rootNode: ASDisplayNode, externalKnobSurface: UIView? = nil, performAction: @escaping (NSAttributedString, TextSelectionAction) -> Void) { self.theme = theme self.strings = strings self.textNode = textNode @@ -269,8 +269,13 @@ public final class TextSelectionNode: ASDisplayNode { return TextSelectionNodeView() }) - self.addSubnode(self.leftKnob) - self.addSubnode(self.rightKnob) + if let externalKnobSurface { + externalKnobSurface.addSubnode(self.leftKnob) + externalKnobSurface.addSubnode(self.rightKnob) + } else { + self.addSubnode(self.leftKnob) + self.addSubnode(self.rightKnob) + } } override public func didLoad() { @@ -449,9 +454,11 @@ public final class TextSelectionNode: ASDisplayNode { } else { highlightOverlay = LinkHighlightingNode(color: self.theme.selection) highlightOverlay.isUserInteractionEnabled = false - highlightOverlay.innerRadius = 0.0 - highlightOverlay.outerRadius = 0.0 + highlightOverlay.innerRadius = 2.0 + highlightOverlay.outerRadius = 2.0 highlightOverlay.inset = 1.0 + highlightOverlay.useModernPathCalculation = true + self.highlightOverlay = highlightOverlay self.highlightAreaNode.addSubnode(highlightOverlay) }