mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Various improvements
This commit is contained in:
@@ -8,30 +8,18 @@ import AccountContext
|
||||
import TelegramPresentationData
|
||||
import PeerListItemComponent
|
||||
|
||||
extension ChatPresentationInputQueryResult {
|
||||
var count: Int {
|
||||
switch self {
|
||||
case let .stickers(stickers):
|
||||
return stickers.count
|
||||
case let .hashtags(hashtags):
|
||||
return hashtags.count
|
||||
case let .mentions(peers):
|
||||
return peers.count
|
||||
case let .commands(commands):
|
||||
return commands.count
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class ContextResultPanelComponent: Component {
|
||||
final class ExternalState {
|
||||
fileprivate(set) var minimizedHeight: CGFloat = 0.0
|
||||
fileprivate(set) var effectiveHeight: CGFloat = 0.0
|
||||
|
||||
init() {
|
||||
enum Results: Equatable {
|
||||
case mentions([EnginePeer])
|
||||
case hashtags([String])
|
||||
|
||||
var count: Int {
|
||||
switch self {
|
||||
case let .hashtags(hashtags):
|
||||
return hashtags.count
|
||||
case let .mentions(peers):
|
||||
return peers.count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,22 +28,19 @@ final class ContextResultPanelComponent: Component {
|
||||
case hashtag(String)
|
||||
}
|
||||
|
||||
let externalState: ExternalState
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let results: ChatPresentationInputQueryResult
|
||||
let results: Results
|
||||
let action: (ResultAction) -> Void
|
||||
|
||||
init(
|
||||
externalState: ExternalState,
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
results: ChatPresentationInputQueryResult,
|
||||
results: Results,
|
||||
action: @escaping (ResultAction) -> Void
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@@ -64,9 +49,6 @@ final class ContextResultPanelComponent: Component {
|
||||
}
|
||||
|
||||
static func ==(lhs: ContextResultPanelComponent, rhs: ContextResultPanelComponent) -> Bool {
|
||||
if lhs.externalState !== rhs.externalState {
|
||||
return false
|
||||
}
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
@@ -87,27 +69,27 @@ final class ContextResultPanelComponent: Component {
|
||||
var bottomInset: CGFloat
|
||||
var topInset: CGFloat
|
||||
var sideInset: CGFloat
|
||||
var itemHeight: CGFloat
|
||||
var itemSize: CGSize
|
||||
var itemCount: Int
|
||||
|
||||
var contentSize: CGSize
|
||||
|
||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemHeight: CGFloat, itemCount: Int) {
|
||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.bottomInset = bottomInset
|
||||
self.topInset = topInset
|
||||
self.sideInset = sideInset
|
||||
self.itemHeight = itemHeight
|
||||
self.itemSize = itemSize
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemHeight + bottomInset)
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow
|
||||
let maxVisibleIndex = maxVisibleRow
|
||||
@@ -120,7 +102,7 @@ final class ContextResultPanelComponent: Component {
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerSize.width, height: self.itemHeight))
|
||||
return CGRect(origin: CGPoint(x: 0.0, y: self.topInset + CGFloat(index) * self.itemSize.height), size: CGSize(width: self.containerSize.width, height: self.itemSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +141,7 @@ final class ContextResultPanelComponent: Component {
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.indicatorStyle = .white
|
||||
@@ -329,7 +311,7 @@ final class ContextResultPanelComponent: Component {
|
||||
bottomInset: 0.0,
|
||||
topInset: 0.0,
|
||||
sideInset: sideInset,
|
||||
itemHeight: measureItemSize.height,
|
||||
itemSize: measureItemSize,
|
||||
itemCount: component.results.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
@@ -357,11 +339,6 @@ final class ContextResultPanelComponent: Component {
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
// component.externalState.minimizedHeight = minimizedHeight
|
||||
|
||||
// let effectiveHeight: CGFloat = minimizedHeight * dismissFraction + (1.0 - dismissFraction) * (60.0 + component.safeInsets.bottom + 1.0)
|
||||
// component.externalState.effectiveHeight = min(minimizedHeight, max(0.0, effectiveHeight))
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import TelegramCore
|
||||
import TextFieldComponent
|
||||
import ChatContextQuery
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
|
||||
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||
@@ -40,7 +41,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr
|
||||
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||
let inputQueries = inputContextQueries(inputState).filter({ query in
|
||||
switch query {
|
||||
case .contextRequest, .command, .emoji:
|
||||
case .contextRequest, .command:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
@@ -75,6 +76,57 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
|
||||
|
||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||
switch inputQuery {
|
||||
case let .emoji(query):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
if let previousQuery = previousQuery {
|
||||
switch previousQuery {
|
||||
case .emoji:
|
||||
break
|
||||
default:
|
||||
signal = .single({ _ in return .stickers([]) })
|
||||
}
|
||||
} else {
|
||||
signal = .single({ _ in return .stickers([]) })
|
||||
}
|
||||
|
||||
let stickerConfiguration = context.account.postbox.preferencesView(keys: [PreferencesKeys.appConfiguration])
|
||||
|> map { preferencesView -> StickersSearchConfiguration in
|
||||
let appConfiguration: AppConfiguration = preferencesView.values[PreferencesKeys.appConfiguration]?.get(AppConfiguration.self) ?? .defaultValue
|
||||
return StickersSearchConfiguration.with(appConfiguration: appConfiguration)
|
||||
}
|
||||
let stickerSettings = context.sharedContext.accountManager.transaction { transaction -> StickerSettings in
|
||||
let stickerSettings: StickerSettings = transaction.getSharedData(ApplicationSpecificSharedDataKeys.stickerSettings)?.get(StickerSettings.self) ?? .defaultSettings
|
||||
return stickerSettings
|
||||
}
|
||||
|
||||
let stickers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = combineLatest(stickerConfiguration, stickerSettings)
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|> mapToSignal { stickerConfiguration, stickerSettings -> Signal<[FoundStickerItem], ChatContextQueryError> in
|
||||
let scope: SearchStickersScope
|
||||
switch stickerSettings.emojiStickerSuggestionMode {
|
||||
case .none:
|
||||
scope = []
|
||||
case .all:
|
||||
if stickerConfiguration.disableLocalSuggestions {
|
||||
scope = [.remote]
|
||||
} else {
|
||||
scope = [.installed, .remote]
|
||||
}
|
||||
case .installed:
|
||||
scope = [.installed]
|
||||
}
|
||||
return context.engine.stickers.searchStickers(query: [query.basicEmoji.0], scope: scope)
|
||||
|> map { items -> [FoundStickerItem] in
|
||||
return items.items
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
}
|
||||
|> map { stickers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
return { _ in
|
||||
return .stickers(stickers)
|
||||
}
|
||||
}
|
||||
return signal |> then(stickers)
|
||||
case let .hashtag(query):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
if let previousQuery = previousQuery {
|
||||
|
||||
@@ -52,7 +52,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let nextInputMode: (Bool) -> InputMode?
|
||||
public let areVoiceMessagesAvailable: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
public let presentInGlobalOverlay: (ViewController) -> Void
|
||||
public let sendMessageAction: () -> Void
|
||||
public let sendStickerAction: (TelegramMediaFile) -> Void
|
||||
public let setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?
|
||||
public let lockMediaRecording: (() -> Void)?
|
||||
public let stopAndPreviewMediaRecording: (() -> Void)?
|
||||
@@ -73,6 +75,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let displayGradient: Bool
|
||||
public let bottomInset: CGFloat
|
||||
public let hideKeyboard: Bool
|
||||
public let forceIsEditing: Bool
|
||||
public let disabledPlaceholder: String?
|
||||
|
||||
public init(
|
||||
@@ -86,7 +89,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
nextInputMode: @escaping (Bool) -> InputMode?,
|
||||
areVoiceMessagesAvailable: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
presentInGlobalOverlay: @escaping (ViewController) -> Void,
|
||||
sendMessageAction: @escaping () -> Void,
|
||||
sendStickerAction: @escaping (TelegramMediaFile) -> Void,
|
||||
setMediaRecordingActive: ((Bool, Bool, Bool) -> Void)?,
|
||||
lockMediaRecording: (() -> Void)?,
|
||||
stopAndPreviewMediaRecording: (() -> Void)?,
|
||||
@@ -107,6 +112,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
displayGradient: Bool,
|
||||
bottomInset: CGFloat,
|
||||
hideKeyboard: Bool,
|
||||
forceIsEditing: Bool,
|
||||
disabledPlaceholder: String?
|
||||
) {
|
||||
self.externalState = externalState
|
||||
@@ -119,7 +125,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||
self.presentController = presentController
|
||||
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||
self.sendMessageAction = sendMessageAction
|
||||
self.sendStickerAction = sendStickerAction
|
||||
self.setMediaRecordingActive = setMediaRecordingActive
|
||||
self.lockMediaRecording = lockMediaRecording
|
||||
self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording
|
||||
@@ -140,6 +148,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.displayGradient = displayGradient
|
||||
self.bottomInset = bottomInset
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.forceIsEditing = forceIsEditing
|
||||
self.disabledPlaceholder = disabledPlaceholder
|
||||
}
|
||||
|
||||
@@ -204,6 +213,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.hideKeyboard != rhs.hideKeyboard {
|
||||
return false
|
||||
}
|
||||
if lhs.forceIsEditing != rhs.forceIsEditing {
|
||||
return false
|
||||
}
|
||||
if lhs.disabledPlaceholder != rhs.disabledPlaceholder {
|
||||
return false
|
||||
}
|
||||
@@ -248,7 +260,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
private var contextQueryStates: [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)] = [:]
|
||||
private var contextQueryResults: [ChatPresentationInputQueryKind: ChatPresentationInputQueryResult] = [:]
|
||||
private var contextQueryResultPanel: ComponentView<Empty>?
|
||||
private var contextQueryResultPanelExternalState: ContextResultPanelComponent.ExternalState?
|
||||
|
||||
private var stickersResultPanel: ComponentView<Empty>?
|
||||
|
||||
private var viewForOverlayContent: ViewForOverlayContent?
|
||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||
@@ -389,6 +402,10 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.state?.updated()
|
||||
}
|
||||
|
||||
if result == nil, let stickersResultPanel = self.stickersResultPanel?.view, let panelResult = stickersResultPanel.hitTest(self.convert(point, to: stickersResultPanel), with: event), panelResult !== stickersResultPanel {
|
||||
return panelResult
|
||||
}
|
||||
|
||||
if result == nil, let contextQueryResultPanel = self.contextQueryResultPanel?.view, let panelResult = contextQueryResultPanel.hitTest(self.convert(point, to: contextQueryResultPanel), with: event), panelResult !== contextQueryResultPanel {
|
||||
return panelResult
|
||||
}
|
||||
@@ -471,6 +488,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
||||
|
||||
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
@@ -493,7 +512,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil {
|
||||
if !isEditing && component.setMediaRecordingActive == nil {
|
||||
insets.right = insets.left
|
||||
}
|
||||
|
||||
@@ -518,7 +537,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setAlpha(view: self.bottomGradientView, alpha: component.displayGradient ? 1.0 : 0.0)
|
||||
|
||||
let placeholderOriginX: CGFloat
|
||||
if self.textFieldExternalState.isEditing || component.style == .story {
|
||||
if isEditing || component.style == .story {
|
||||
placeholderOriginX = 16.0
|
||||
} else {
|
||||
placeholderOriginX = floorToScreenPixels((availableSize.width - placeholderSize.width) / 2.0)
|
||||
@@ -729,14 +748,14 @@ public final class MessageInputPanelComponent: Component {
|
||||
|
||||
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
|
||||
if case .editor = component.style {
|
||||
inputActionButtonMode = self.textFieldExternalState.isEditing ? .apply : .none
|
||||
inputActionButtonMode = isEditing ? .apply : .none
|
||||
} else {
|
||||
if hasMediaEditing {
|
||||
inputActionButtonMode = .send
|
||||
} else {
|
||||
if self.textFieldExternalState.hasText {
|
||||
inputActionButtonMode = .send
|
||||
} else if !self.textFieldExternalState.isEditing && component.forwardAction != nil {
|
||||
} else if !isEditing && component.forwardAction != nil {
|
||||
inputActionButtonMode = .forward
|
||||
} else {
|
||||
if component.areVoiceMessagesAvailable {
|
||||
@@ -831,7 +850,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.addSubview(inputActionButtonView)
|
||||
}
|
||||
let inputActionButtonOriginX: CGFloat
|
||||
if component.setMediaRecordingActive != nil || self.textFieldExternalState.isEditing {
|
||||
if component.setMediaRecordingActive != nil || isEditing {
|
||||
inputActionButtonOriginX = size.width - insets.right + floorToScreenPixels((insets.right - inputActionButtonSize.width) * 0.5)
|
||||
} else {
|
||||
inputActionButtonOriginX = size.width
|
||||
@@ -845,7 +864,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
var fieldIconNextX = fieldBackgroundFrame.maxX - 4.0
|
||||
|
||||
var inputModeVisible = false
|
||||
if component.style == .story || self.textFieldExternalState.isEditing {
|
||||
if component.style == .story || isEditing {
|
||||
inputModeVisible = true
|
||||
}
|
||||
|
||||
@@ -996,15 +1015,15 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
transition.setAlpha(view: timeoutButtonView, alpha: self.textFieldExternalState.isEditing ? 0.0 : 1.0)
|
||||
transition.setScale(view: timeoutButtonView, scale: self.textFieldExternalState.isEditing ? 0.1 : 1.0)
|
||||
transition.setAlpha(view: timeoutButtonView, alpha: isEditing ? 0.0 : 1.0)
|
||||
transition.setScale(view: timeoutButtonView, scale: isEditing ? 0.1 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
var fieldBackgroundIsDark = false
|
||||
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||
fieldBackgroundIsDark = true
|
||||
} else if self.textFieldExternalState.isEditing || component.style == .editor {
|
||||
} else if isEditing || component.style == .editor {
|
||||
fieldBackgroundIsDark = true
|
||||
}
|
||||
self.fieldBackgroundView.updateColor(color: fieldBackgroundIsDark ? UIColor(white: 0.0, alpha: 0.5) : UIColor(white: 1.0, alpha: 0.09), transition: transition.containedViewLayoutTransition)
|
||||
@@ -1013,7 +1032,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
vibrancyPlaceholderView.isHidden = placeholder.isHidden
|
||||
}
|
||||
|
||||
component.externalState.isEditing = self.textFieldExternalState.isEditing
|
||||
component.externalState.isEditing = isEditing
|
||||
component.externalState.hasText = self.textFieldExternalState.hasText
|
||||
component.externalState.insertText = { [weak self] text in
|
||||
if let self, let view = self.textField.view as? TextFieldComponent.View {
|
||||
@@ -1177,63 +1196,128 @@ public final class MessageInputPanelComponent: Component {
|
||||
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||
|
||||
if let result = self.contextQueryResults[.mention], result.count > 0 && self.textFieldExternalState.isEditing {
|
||||
var contextResults: ContextResultPanelComponent.Results?
|
||||
if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty {
|
||||
contextResults = .mentions(mentions)
|
||||
}
|
||||
|
||||
if let result = self.contextQueryResults[.emoji], case let .stickers(stickers) = result, !stickers.isEmpty {
|
||||
let availablePanelHeight: CGFloat = 413.0
|
||||
|
||||
var animateIn = false
|
||||
let panel: ComponentView<Empty>
|
||||
let externalState: ContextResultPanelComponent.ExternalState
|
||||
var transition = transition
|
||||
if let current = self.contextQueryResultPanel, let currentState = self.contextQueryResultPanelExternalState {
|
||||
if let current = self.stickersResultPanel {
|
||||
panel = current
|
||||
} else {
|
||||
panel = ComponentView<Empty>()
|
||||
self.stickersResultPanel = panel
|
||||
animateIn = true
|
||||
transition = .immediate
|
||||
}
|
||||
let panelSize = panel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StickersResultPanelComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
files: stickers.map { $0.file },
|
||||
action: { [weak self] sticker in
|
||||
if let self, let textView = self.textField.view as? TextFieldComponent.View {
|
||||
textView.updateText(NSAttributedString(), selectionRange: 0 ..< 0)
|
||||
self.component?.sendStickerAction(sticker)
|
||||
}
|
||||
},
|
||||
present: { [weak self] c in
|
||||
if let self, let component = self.component {
|
||||
component.presentController(c)
|
||||
}
|
||||
},
|
||||
presentInGlobalOverlay: { [weak self] c in
|
||||
if let self, let component = self.component {
|
||||
component.presentInGlobalOverlay(c)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availablePanelHeight)
|
||||
)
|
||||
|
||||
let panelFrame = CGRect(origin: CGPoint(x: 0.0, y: -panelSize.height + 60.0), size: panelSize)
|
||||
if let panelView = panel.view as? StickersResultPanelComponent.View {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, at: 0)
|
||||
}
|
||||
transition.setFrame(view: panelView, frame: panelFrame)
|
||||
|
||||
if animateIn {
|
||||
panelView.animateIn(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
} else if let stickersResultPanel = self.stickersResultPanel?.view as? StickersResultPanelComponent.View {
|
||||
self.stickersResultPanel = nil
|
||||
stickersResultPanel.animateOut(transition: .spring(duration: 0.4), completion: { [weak stickersResultPanel] in
|
||||
stickersResultPanel?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
|
||||
if let contextResults, isEditing {
|
||||
let availablePanelHeight: CGFloat = 413.0
|
||||
|
||||
var animateIn = false
|
||||
let panel: ComponentView<Empty>
|
||||
var transition = transition
|
||||
if let current = self.contextQueryResultPanel {
|
||||
panel = current
|
||||
externalState = currentState
|
||||
} else {
|
||||
panel = ComponentView<Empty>()
|
||||
externalState = ContextResultPanelComponent.ExternalState()
|
||||
self.contextQueryResultPanel = panel
|
||||
self.contextQueryResultPanelExternalState = externalState
|
||||
animateIn = true
|
||||
transition = .immediate
|
||||
}
|
||||
let panelSize = panel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ContextResultPanelComponent(
|
||||
externalState: externalState,
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
results: result,
|
||||
results: contextResults,
|
||||
action: { [weak self] action in
|
||||
if let self, case let .mention(peer) = action, let textView = self.textField.view as? TextFieldComponent.View {
|
||||
if let self, let textView = self.textField.view as? TextFieldComponent.View {
|
||||
let inputState = textView.getInputState()
|
||||
|
||||
var mentionQueryRange: NSRange?
|
||||
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
|
||||
if type == [.mention] {
|
||||
mentionQueryRange = range
|
||||
break inner
|
||||
switch action {
|
||||
case let .mention(peer):
|
||||
var mentionQueryRange: NSRange?
|
||||
inner: for (range, type, _) in textInputStateContextQueryRangeAndType(inputState: inputState) {
|
||||
if type == [.mention] {
|
||||
mentionQueryRange = range
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let range = mentionQueryRange {
|
||||
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
let replacementText = addressName + " "
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
|
||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
} else if !peer.compactDisplayTitle.isEmpty {
|
||||
let replacementText = NSMutableAttributedString()
|
||||
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
||||
replacementText.append(NSAttributedString(string: " "))
|
||||
|
||||
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
|
||||
inputText.replaceCharacters(in: updatedRange, with: replacementText)
|
||||
|
||||
let selectionPosition = updatedRange.lowerBound + replacementText.length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
|
||||
if let range = mentionQueryRange {
|
||||
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||
if let addressName = peer.addressName, !addressName.isEmpty {
|
||||
let replacementText = addressName + " "
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
|
||||
let selectionPosition = range.lowerBound + (replacementText as NSString).length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
} else if !peer.compactDisplayTitle.isEmpty {
|
||||
let replacementText = NSMutableAttributedString()
|
||||
replacementText.append(NSAttributedString(string: peer.compactDisplayTitle, attributes: [ChatTextInputAttributes.textMention: ChatTextInputTextMentionAttribute(peerId: peer.id)]))
|
||||
replacementText.append(NSAttributedString(string: " "))
|
||||
|
||||
let updatedRange = NSRange(location: range.location - 1, length: range.length + 1)
|
||||
inputText.replaceCharacters(in: updatedRange, with: replacementText)
|
||||
|
||||
let selectionPosition = updatedRange.lowerBound + replacementText.length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
}
|
||||
case let .hashtag(hashtag):
|
||||
let _ = hashtag
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1309,7 +1393,6 @@ public final class MessageInputPanelComponent: Component {
|
||||
|
||||
//self.installEmojiSuggestionPreviewGesture(hostView: currentEmojiSuggestionView)
|
||||
}
|
||||
|
||||
|
||||
let globalPosition: CGPoint
|
||||
if let textView = self.textField.view {
|
||||
|
||||
@@ -0,0 +1,528 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramPresentationData
|
||||
import PeerListItemComponent
|
||||
import EmojiTextAttachmentView
|
||||
import TextFormat
|
||||
import ContextUI
|
||||
import StickerPeekUI
|
||||
import UndoUI
|
||||
|
||||
final class StickersResultPanelComponent: Component {
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let files: [TelegramMediaFile]
|
||||
let action: (TelegramMediaFile) -> Void
|
||||
let present: (ViewController) -> Void
|
||||
let presentInGlobalOverlay: (ViewController) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
files: [TelegramMediaFile],
|
||||
action: @escaping (TelegramMediaFile) -> Void,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
presentInGlobalOverlay: @escaping (ViewController) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.files = files
|
||||
self.action = action
|
||||
self.present = present
|
||||
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||
}
|
||||
|
||||
static func ==(lhs: StickersResultPanelComponent, rhs: StickersResultPanelComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.files != rhs.files {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private struct ItemLayout: Equatable {
|
||||
var containerSize: CGSize
|
||||
var bottomInset: CGFloat
|
||||
var topInset: CGFloat
|
||||
var sideInset: CGFloat
|
||||
var itemSize: CGSize
|
||||
var itemSpacing: CGFloat
|
||||
var itemsPerRow: Int
|
||||
var itemCount: Int
|
||||
|
||||
var contentSize: CGSize
|
||||
|
||||
init(containerSize: CGSize, bottomInset: CGFloat, topInset: CGFloat, sideInset: CGFloat, itemSize: CGSize, itemSpacing: CGFloat, itemsPerRow: Int, itemCount: Int) {
|
||||
self.containerSize = containerSize
|
||||
self.bottomInset = bottomInset
|
||||
self.topInset = topInset
|
||||
self.sideInset = sideInset
|
||||
self.itemSize = itemSize
|
||||
self.itemSpacing = itemSpacing
|
||||
self.itemsPerRow = itemsPerRow
|
||||
self.itemCount = itemCount
|
||||
|
||||
self.contentSize = CGSize(width: containerSize.width, height: topInset + CGFloat(itemCount) * itemSize.height + bottomInset)
|
||||
}
|
||||
|
||||
func visibleItems(for rect: CGRect) -> Range<Int>? {
|
||||
let offsetRect = rect.offsetBy(dx: 0.0, dy: -self.topInset)
|
||||
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemSize.height + self.itemSpacing)))
|
||||
minVisibleRow = max(0, minVisibleRow)
|
||||
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemSize.height + self.itemSpacing)))
|
||||
|
||||
let minVisibleIndex = minVisibleRow * self.itemsPerRow
|
||||
let maxVisibleIndex = maxVisibleRow * self.itemsPerRow + self.itemsPerRow
|
||||
|
||||
if maxVisibleIndex >= minVisibleIndex {
|
||||
return minVisibleIndex ..< (maxVisibleIndex + 1)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func itemFrame(for index: Int) -> CGRect {
|
||||
let rowIndex = Int(floor(CGFloat(index) / CGFloat(self.itemsPerRow)))
|
||||
let columnIndex = index % self.itemsPerRow
|
||||
|
||||
return CGRect(origin: CGPoint(x: self.sideInset + CGFloat(columnIndex) * (self.itemSize.width + self.itemSpacing), y: self.topInset + CGFloat(rowIndex) * (self.itemSize.height + self.itemSpacing)), size: self.itemSize)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class View: UIView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let containerView: UIView
|
||||
private let scrollView: UIScrollView
|
||||
|
||||
private var itemLayout: ItemLayout?
|
||||
|
||||
private var visibleLayers: [EngineMedia.Id: InlineStickerItemLayer] = [:]
|
||||
private var fadingMaskLayer: FadingMaskLayer?
|
||||
|
||||
private var ignoreScrolling = false
|
||||
|
||||
private var component: StickersResultPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: .clear, enableBlur: true)
|
||||
self.backgroundView.isUserInteractionEnabled = false
|
||||
|
||||
self.containerView = UIView()
|
||||
|
||||
self.scrollView = ScrollView()
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.alwaysBounceVertical = true
|
||||
self.scrollView.indicatorStyle = .white
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.scrollView.delegate = self
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.containerView)
|
||||
self.containerView.addSubview(self.scrollView)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
|
||||
let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in
|
||||
if let self, let component = self.component {
|
||||
let presentationData = component.strings
|
||||
|
||||
let convertedPoint = self.scrollView.convert(point, from: self)
|
||||
guard self.scrollView.bounds.contains(convertedPoint) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var selectedLayer: InlineStickerItemLayer?
|
||||
for (_, layer) in self.visibleLayers {
|
||||
if layer.frame.contains(convertedPoint) {
|
||||
selectedLayer = layer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let selectedLayer, let file = selectedLayer.file {
|
||||
return component.context.engine.stickers.isStickerSaved(id: file.fileId)
|
||||
|> deliverOnMainQueue
|
||||
|> map { [weak self] isStarred -> (UIView, CGRect, PeekControllerContent)? in
|
||||
if let self, let component = self.component {
|
||||
let menuItems: [ContextMenuItem] = []
|
||||
let _ = menuItems
|
||||
let _ = presentationData
|
||||
// if strongSelf.peerId != strongSelf.context.account.peerId && strongSelf.peerId?.namespace != Namespaces.Peer.SecretChat {
|
||||
// menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor)
|
||||
// }, action: { _, f in
|
||||
// if let strongSelf = self, let peekController = strongSelf.peekController {
|
||||
// if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
|
||||
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, animationNode.view, animationNode.bounds, nil, [])
|
||||
// } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
|
||||
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, true, imageNode.view, imageNode.bounds, nil, [])
|
||||
// }
|
||||
// }
|
||||
// f(.default)
|
||||
// })))
|
||||
// }
|
||||
//
|
||||
// menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
|
||||
// return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor)
|
||||
// }, action: { _, f in
|
||||
// if let strongSelf = self, let peekController = strongSelf.peekController {
|
||||
// if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode {
|
||||
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, animationNode.view, animationNode.bounds, nil, [])
|
||||
// } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode {
|
||||
// let _ = controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, true, imageNode.view, imageNode.bounds, nil, [])
|
||||
// }
|
||||
// }
|
||||
// f(.default)
|
||||
// })))
|
||||
|
||||
// menuItems.append(
|
||||
// .action(ContextMenuActionItem(text: isStarred ? presentationData.strings.Stickers_RemoveFromFavorites : presentationData.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
//
|
||||
// if let self, let component = self.component {
|
||||
// let _ = (component.context.engine.stickers.toggleStickerSaved(file: file, saved: !isStarred)
|
||||
// |> deliverOnMainQueue).start(next: { [weak self] result in
|
||||
// guard let self, let component = self.component else {
|
||||
// return
|
||||
// }
|
||||
// switch result {
|
||||
// case .generic:
|
||||
// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: nil, text: !isStarred ? presentationData.strings.Conversation_StickerAddedToFavorites : presentationData.strings.Conversation_StickerRemovedFromFavorites, undoText: nil, customAction: nil), elevatedLayout: false, action: { _ in return false })
|
||||
// component.presentInGlobalOverlay(controller)
|
||||
// case let .limitExceeded(limit, premiumLimit):
|
||||
// let premiumConfiguration = PremiumConfiguration.with(appConfiguration: component.context.currentAppConfiguration.with { $0 })
|
||||
// let text: String
|
||||
// if limit == premiumLimit || premiumConfiguration.isPremiumDisabled {
|
||||
// text = presentationData.strings.Premium_MaxFavedStickersFinalText
|
||||
// } else {
|
||||
// text = presentationData.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string
|
||||
// }
|
||||
//
|
||||
// let controller = UndoOverlayController(presentationData: presentationData, content: .sticker(context: component.context, file: file, loop: true, title: presentationData.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil, customAction: nil), elevatedLayout: false, action: { [weak self] action in
|
||||
// if let self, let component = self.component {
|
||||
// if case .info = action {
|
||||
// let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .savedStickers)
|
||||
// // strongSelf.getControllerInteraction?()?.navigationController()?.pushViewController(controller)
|
||||
// return true
|
||||
// }
|
||||
// }
|
||||
// return false
|
||||
// })
|
||||
// component.presentInGlobalOverlay(controller)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// }))
|
||||
// )
|
||||
//
|
||||
// menuItems.append(
|
||||
// .action(ContextMenuActionItem(text: presentationData.strings.StickerPack_ViewPack, icon: { theme in generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in
|
||||
// f(.default)
|
||||
//
|
||||
// if let self, let component = self.component {
|
||||
// loop: for attribute in file.attributes {
|
||||
// switch attribute {
|
||||
// case let .Sticker(_, packReference, _):
|
||||
// if let packReference = packReference {
|
||||
// let controller = component.context.sharedContext.makeStickerPackScreen(context: component.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: nil, sendSticker: { [weak self] file, sourceNode, sourceRect in
|
||||
// if let self, let component = self.component {
|
||||
// component.action(file)
|
||||
// return true
|
||||
// } else {
|
||||
// return false
|
||||
// }
|
||||
// })
|
||||
// component.present(controller)
|
||||
// }
|
||||
// break loop
|
||||
// default:
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }))
|
||||
// )
|
||||
return (self, selectedLayer.frame, StickerPreviewPeekContent(context: component.context, theme: component.theme, strings: component.strings, item: .pack(file), menu: menuItems, openPremiumIntro: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
let controller = component.context.sharedContext.makePremiumIntroController(context: component.context, source: .stickers)
|
||||
component.present(controller)
|
||||
// let controller = PremiumIntroScreen(context: component.context, source: .stickers)
|
||||
// controllerInteraction.navigationController()?.pushViewController(controller)
|
||||
}))
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, present: { [weak self] content, sourceView, sourceRect in
|
||||
if let self, let component = self.component {
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = PeekController(presentationData: presentationData, content: content, sourceView: {
|
||||
return (sourceView, sourceRect)
|
||||
})
|
||||
// controller.visibilityUpdated = { [weak self] visible in
|
||||
// self?.previewingStickersPromise.set(visible)
|
||||
// }
|
||||
component.presentInGlobalOverlay(controller)
|
||||
// strongSelf.peekController = controller
|
||||
// strongSelf.getControllerInteraction?()?.presentGlobalOverlayController(controller, nil)
|
||||
return controller
|
||||
}
|
||||
return nil
|
||||
}, updateContent: { [weak self] content in
|
||||
if let self {
|
||||
var item: TelegramMediaFile?
|
||||
if let content = content as? StickerPreviewPeekContent, case let .pack(contentItem) = content.item {
|
||||
item = contentItem
|
||||
}
|
||||
let _ = item
|
||||
let _ = self
|
||||
//strongSelf.updatePreviewingItem(file: item, animated: true)
|
||||
}
|
||||
})
|
||||
self.addGestureRecognizer(peekRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let location = recognizer.location(in: self.scrollView)
|
||||
if self.scrollView.bounds.contains(location) {
|
||||
var closestFile: (file: TelegramMediaFile, distance: CGFloat)?
|
||||
for (_, itemLayer) in self.visibleLayers {
|
||||
guard let file = itemLayer.file else {
|
||||
continue
|
||||
}
|
||||
if itemLayer.frame.contains(location) {
|
||||
closestFile = (file, 0.0)
|
||||
}
|
||||
}
|
||||
if let (file, _) = closestFile {
|
||||
self.component?.action(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(transition: Transition) {
|
||||
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||
Transition.immediate.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset))
|
||||
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: 0.0))
|
||||
}
|
||||
|
||||
func animateOut(transition: Transition, completion: @escaping () -> Void) {
|
||||
let offset = self.scrollView.contentOffset.y * -1.0 + 10.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
transition.setBoundsOrigin(view: self, origin: CGPoint(x: 0.0, y: -offset), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if !self.ignoreScrolling {
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateScrolling(transition: Transition) {
|
||||
guard let component = self.component, let itemLayout = self.itemLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -200.0)
|
||||
|
||||
var synchronousLoad = false
|
||||
if let hint = transition.userData(PeerListItemComponent.TransitionHint.self) {
|
||||
synchronousLoad = hint.synchronousLoad
|
||||
}
|
||||
|
||||
var visibleIds = Set<EngineMedia.Id>()
|
||||
if let range = itemLayout.visibleItems(for: visibleBounds) {
|
||||
for index in range.lowerBound ..< range.upperBound {
|
||||
guard index < component.files.count else {
|
||||
continue
|
||||
}
|
||||
|
||||
let itemFrame = itemLayout.itemFrame(for: index)
|
||||
|
||||
let item = component.files[index]
|
||||
visibleIds.insert(item.fileId)
|
||||
|
||||
let itemLayer: InlineStickerItemLayer
|
||||
if let current = self.visibleLayers[item.fileId] {
|
||||
itemLayer = current
|
||||
itemLayer.dynamicColor = .white
|
||||
} else {
|
||||
itemLayer = InlineStickerItemLayer(
|
||||
context: component.context,
|
||||
userLocation: .other,
|
||||
attemptSynchronousLoad: synchronousLoad,
|
||||
emoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: item.fileId.id, file: item),
|
||||
file: item,
|
||||
cache: component.context.animationCache,
|
||||
renderer: component.context.animationRenderer,
|
||||
placeholderColor: UIColor(rgb: 0xffffff).mixedWith(UIColor(rgb: 0x1c1c1d), alpha: 0.9),
|
||||
pointSize: itemFrame.size,
|
||||
dynamicColor: .white
|
||||
)
|
||||
self.visibleLayers[item.fileId] = itemLayer
|
||||
self.scrollView.layer.addSublayer(itemLayer)
|
||||
}
|
||||
|
||||
itemLayer.frame = itemFrame
|
||||
|
||||
itemLayer.isVisibleForAnimations = true
|
||||
}
|
||||
}
|
||||
|
||||
var removedIds: [EngineMedia.Id] = []
|
||||
for (id, itemLayer) in self.visibleLayers {
|
||||
if !visibleIds.contains(id) {
|
||||
itemLayer.removeFromSuperlayer()
|
||||
removedIds.append(id)
|
||||
}
|
||||
}
|
||||
for id in removedIds {
|
||||
self.visibleLayers.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
let backgroundSize = CGSize(width: self.scrollView.frame.width, height: self.scrollView.frame.height + 20.0)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: max(0.0, self.scrollView.contentOffset.y * -1.0)), size: backgroundSize))
|
||||
self.backgroundView.update(size: backgroundSize, cornerRadius: 11.0, transition: transition.containedViewLayoutTransition)
|
||||
}
|
||||
|
||||
func update(component: StickersResultPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
//let itemUpdated = self.component?.results != component.results
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let minimizedHeight = min(availableSize.height, 500.0)
|
||||
|
||||
self.backgroundView.updateColor(color: UIColor(white: 0.0, alpha: 0.7), transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let itemsPerRow = min(8, max(5, Int(availableSize.width / 80)))
|
||||
let sideInset: CGFloat = 2.0
|
||||
let itemSpacing: CGFloat = 2.0
|
||||
let itemSize = floor((availableSize.width - sideInset * 2.0 - itemSpacing * (CGFloat(itemsPerRow) - 1.0)) / CGFloat(itemsPerRow))
|
||||
|
||||
let itemLayout = ItemLayout(
|
||||
containerSize: CGSize(width: availableSize.width, height: minimizedHeight),
|
||||
bottomInset: 9.0,
|
||||
topInset: 9.0,
|
||||
sideInset: sideInset,
|
||||
itemSize: CGSize(width: itemSize, height: itemSize),
|
||||
itemSpacing: itemSpacing,
|
||||
itemsPerRow: itemsPerRow,
|
||||
itemCount: component.files.count
|
||||
)
|
||||
self.itemLayout = itemLayout
|
||||
|
||||
let scrollContentSize = itemLayout.contentSize
|
||||
|
||||
self.ignoreScrolling = true
|
||||
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.width, height: minimizedHeight)))
|
||||
|
||||
let visibleTopContentHeight = min(scrollContentSize.height, itemSize * 3.0 + 19.0)
|
||||
let topInset = availableSize.height - visibleTopContentHeight
|
||||
|
||||
let scrollContentInsets = UIEdgeInsets(top: topInset, left: 0.0, bottom: 19.0, right: 0.0)
|
||||
let scrollIndicatorInsets = UIEdgeInsets(top: topInset + 17.0, left: 0.0, bottom: 19.0, right: 0.0)
|
||||
if self.scrollView.contentInset != scrollContentInsets {
|
||||
self.scrollView.contentInset = scrollContentInsets
|
||||
}
|
||||
if self.scrollView.scrollIndicatorInsets != scrollIndicatorInsets {
|
||||
self.scrollView.scrollIndicatorInsets = scrollIndicatorInsets
|
||||
}
|
||||
if self.scrollView.contentSize != scrollContentSize {
|
||||
self.scrollView.contentSize = scrollContentSize
|
||||
}
|
||||
|
||||
let maskLayer: FadingMaskLayer
|
||||
if let current = self.fadingMaskLayer {
|
||||
maskLayer = current
|
||||
} else {
|
||||
maskLayer = FadingMaskLayer()
|
||||
self.fadingMaskLayer = maskLayer
|
||||
}
|
||||
if self.containerView.layer.mask == nil {
|
||||
self.containerView.layer.mask = maskLayer
|
||||
}
|
||||
maskLayer.frame = CGRect(origin: .zero, size: self.scrollView.frame.size)
|
||||
|
||||
self.containerView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class FadingMaskLayer: SimpleLayer {
|
||||
let gradientLayer = SimpleLayer()
|
||||
let fillLayer = SimpleLayer()
|
||||
|
||||
override func layoutSublayers() {
|
||||
let gradientHeight: CGFloat = 110.0
|
||||
if self.gradientLayer.contents == nil {
|
||||
self.addSublayer(self.gradientLayer)
|
||||
self.addSublayer(self.fillLayer)
|
||||
|
||||
let gradientImage = generateGradientImage(size: CGSize(width: 1.0, height: gradientHeight), colors: [UIColor.white, UIColor.white, UIColor.white.withAlphaComponent(0.0), UIColor.white.withAlphaComponent(0.0)], locations: [0.0, 0.4, 0.9, 1.0], direction: .vertical)
|
||||
self.gradientLayer.contents = gradientImage?.cgImage
|
||||
self.gradientLayer.contentsGravity = .resize
|
||||
self.fillLayer.backgroundColor = UIColor.white.cgColor
|
||||
}
|
||||
|
||||
self.fillLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.width, height: self.bounds.height - gradientHeight))
|
||||
self.gradientLayer.frame = CGRect(origin: CGPoint(x: 0.0, y: self.bounds.height - gradientHeight), size: CGSize(width: self.bounds.width, height: gradientHeight))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user