mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2026-01-15 00:56:22 +00:00
Cherry-pick media timer and web app improvements
This commit is contained in:
@@ -33,6 +33,11 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView",
|
||||
"//submodules/StickerPeekUI",
|
||||
"//submodules/Components/ReactionButtonListComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/AnimatedCountLabelNode",
|
||||
"//submodules/SearchPeerMembers",
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
||||
@@ -5,6 +5,7 @@ import TextFieldComponent
|
||||
import ChatContextQuery
|
||||
import AccountContext
|
||||
import TelegramUIPreferences
|
||||
import SearchPeerMembers
|
||||
|
||||
func textInputStateContextQueryRangeAndType(inputState: TextFieldComponent.InputState) -> [(NSRange, PossibleContextQueryTypes, NSRange?)] {
|
||||
return textInputStateContextQueryRangeAndType(inputText: inputState.inputText, selectionRange: inputState.selectionRange)
|
||||
@@ -38,7 +39,7 @@ func inputContextQueries(_ inputState: TextFieldComponent.InputState) -> [ChatPr
|
||||
return result
|
||||
}
|
||||
|
||||
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||
func contextQueryResultState(context: AccountContext, inputState: TextFieldComponent.InputState, availableTypes: [ChatPresentationInputQueryKind], chatLocation: ChatLocation?, currentQueryStates: inout [ChatPresentationInputQueryKind: (ChatPresentationInputQuery, Disposable)]) -> [ChatPresentationInputQueryKind: ChatContextQueryUpdate] {
|
||||
let inputQueries = inputContextQueries(inputState).filter({ query in
|
||||
return availableTypes.contains(query.kind)
|
||||
})
|
||||
@@ -48,7 +49,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
|
||||
for query in inputQueries {
|
||||
let previousQuery = currentQueryStates[query.kind]?.0
|
||||
if previousQuery != query {
|
||||
let signal = updatedContextQueryResultStateForQuery(context: context, inputQuery: query, previousQuery: previousQuery)
|
||||
let signal = updatedContextQueryResultStateForQuery(context: context, chatLocation: chatLocation, inputQuery: query, previousQuery: previousQuery)
|
||||
updates[query.kind] = .update(query, signal)
|
||||
}
|
||||
}
|
||||
@@ -69,7 +70,7 @@ func contextQueryResultState(context: AccountContext, inputState: TextFieldCompo
|
||||
return updates
|
||||
}
|
||||
|
||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||
private func updatedContextQueryResultStateForQuery(context: AccountContext, chatLocation: ChatLocation?, inputQuery: ChatPresentationInputQuery, previousQuery: ChatPresentationInputQuery?) -> Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> {
|
||||
switch inputQuery {
|
||||
case let .emoji(query):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
@@ -149,7 +150,7 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
|
||||
|> castError(ChatContextQueryError.self)
|
||||
|
||||
return signal |> then(hashtags)
|
||||
case let .mention(query, _):
|
||||
case let .mention(query, types):
|
||||
var signal: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = .complete()
|
||||
if let previousQuery = previousQuery {
|
||||
switch previousQuery {
|
||||
@@ -163,34 +164,79 @@ private func updatedContextQueryResultStateForQuery(context: AccountContext, inp
|
||||
}
|
||||
|
||||
let normalizedQuery = query.lowercased()
|
||||
if normalizedQuery.isEmpty {
|
||||
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers()
|
||||
|> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
if case let .peers(peers) = recentPeers {
|
||||
let peers = peers.filter { peer in
|
||||
return peer.addressName != nil
|
||||
}.compactMap { EnginePeer($0) }
|
||||
return { _ in return .mentions(peers) }
|
||||
} else {
|
||||
return { _ in return .mentions([]) }
|
||||
}
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
return signal |> then(peers)
|
||||
} else {
|
||||
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery)
|
||||
|> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
let peers = peersAndPresences.filter { peer in
|
||||
if let peer = peer.peer, case .user = peer, peer.addressName != nil {
|
||||
return true
|
||||
} else {
|
||||
|
||||
if let chatLocation, let peerId = chatLocation.peerId {
|
||||
let inlineBots: Signal<[(EnginePeer, Double)], NoError> = types.contains(.contextBots) ? context.engine.peers.recentlyUsedInlineBots() : .single([])
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
let participants = combineLatest(inlineBots, searchPeerMembers(context: context, peerId: peerId, chatLocation: chatLocation, query: query, scope: .mention))
|
||||
|> map { inlineBots, peers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
let filteredInlineBots = inlineBots.sorted(by: { $0.1 > $1.1 }).filter { peer, rating in
|
||||
if rating < 0.14 {
|
||||
return false
|
||||
}
|
||||
}.compactMap { $0.peer }
|
||||
return { _ in return .mentions(peers) }
|
||||
if peer.indexName.matchesByTokens(normalizedQuery) {
|
||||
return true
|
||||
}
|
||||
if let addressName = peer.addressName, addressName.lowercased().hasPrefix(normalizedQuery) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}.map { $0.0 }
|
||||
|
||||
let inlineBotPeerIds = Set(filteredInlineBots.map { $0.id })
|
||||
|
||||
let filteredPeers = peers.filter { peer in
|
||||
if inlineBotPeerIds.contains(peer.id) {
|
||||
return false
|
||||
}
|
||||
if !types.contains(.accountPeer) && peer.id == context.account.peerId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
var sortedPeers = filteredInlineBots
|
||||
sortedPeers.append(contentsOf: filteredPeers.sorted(by: { lhs, rhs in
|
||||
let result = lhs.indexName.stringRepresentation(lastNameFirst: true).compare(rhs.indexName.stringRepresentation(lastNameFirst: true))
|
||||
return result == .orderedAscending
|
||||
}))
|
||||
sortedPeers = sortedPeers.filter { peer in
|
||||
return !peer.displayTitle(strings: strings, displayOrder: .firstLast).isEmpty
|
||||
}
|
||||
return { _ in return .mentions(sortedPeers) }
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
return signal |> then(peers)
|
||||
|
||||
return signal |> then(participants)
|
||||
} else {
|
||||
if normalizedQuery.isEmpty {
|
||||
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.peers.recentPeers()
|
||||
|> map { recentPeers -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
if case let .peers(peers) = recentPeers {
|
||||
let peers = peers.filter { peer in
|
||||
return peer.addressName != nil
|
||||
}.compactMap { EnginePeer($0) }
|
||||
return { _ in return .mentions(peers) }
|
||||
} else {
|
||||
return { _ in return .mentions([]) }
|
||||
}
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
return signal |> then(peers)
|
||||
} else {
|
||||
let peers: Signal<(ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult?, ChatContextQueryError> = context.engine.contacts.searchLocalPeers(query: normalizedQuery)
|
||||
|> map { peersAndPresences -> (ChatPresentationInputQueryResult?) -> ChatPresentationInputQueryResult? in
|
||||
let peers = peersAndPresences.filter { peer in
|
||||
if let peer = peer.peer, case .user = peer, peer.addressName != nil {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}.compactMap { $0.peer }
|
||||
return { _ in return .mentions(peers) }
|
||||
}
|
||||
|> castError(ChatContextQueryError.self)
|
||||
return signal |> then(peers)
|
||||
}
|
||||
}
|
||||
case let .emojiSearch(query, languageCode, range):
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|
||||
@@ -15,6 +15,11 @@ import ChatContextQuery
|
||||
import TextFormat
|
||||
import EmojiSuggestionsComponent
|
||||
import AudioToolbox
|
||||
import AnimatedTextComponent
|
||||
import AnimatedCountLabelNode
|
||||
import ContextReferenceButtonComponent
|
||||
|
||||
private let timeoutButtonTag = GenericComponentViewTag()
|
||||
|
||||
public final class MessageInputPanelComponent: Component {
|
||||
public struct ContextQueryTypes: OptionSet {
|
||||
@@ -36,6 +41,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public enum Style {
|
||||
case story
|
||||
case editor
|
||||
case media
|
||||
}
|
||||
|
||||
public enum InputMode: Hashable {
|
||||
@@ -56,6 +62,26 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public enum Placeholder: Equatable {
|
||||
public enum CounterItemContent: Equatable {
|
||||
case text(String)
|
||||
case number(Int, minDigits: Int)
|
||||
}
|
||||
|
||||
public struct CounterItem: Equatable {
|
||||
public var id: Int
|
||||
public var content: CounterItemContent
|
||||
|
||||
public init(id: Int, content: CounterItemContent) {
|
||||
self.id = id
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
case plain(String)
|
||||
case counter([CounterItem])
|
||||
}
|
||||
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var isEditing: Bool = false
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
@@ -75,10 +101,11 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
public let style: Style
|
||||
public let placeholder: String
|
||||
public let placeholder: Placeholder
|
||||
public let maxLength: Int?
|
||||
public let queryTypes: ContextQueryTypes
|
||||
public let alwaysDarkWhenHasText: Bool
|
||||
public let resetInputContents: SendMessageInput?
|
||||
public let nextInputMode: (Bool) -> InputMode?
|
||||
public let areVoiceMessagesAvailable: Bool
|
||||
public let presentController: (ViewController) -> Void
|
||||
@@ -95,7 +122,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let likeAction: (() -> Void)?
|
||||
public let likeOptionsAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let inputModeAction: (() -> Void)?
|
||||
public let timeoutAction: ((UIView) -> Void)?
|
||||
public let timeoutAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let forwardAction: (() -> Void)?
|
||||
public let moreAction: ((UIView, ContextGesture?) -> Void)?
|
||||
public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?
|
||||
@@ -116,7 +143,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
public let hideKeyboard: Bool
|
||||
public let forceIsEditing: Bool
|
||||
public let disabledPlaceholder: String?
|
||||
public let storyId: Int32?
|
||||
public let isChannel: Bool
|
||||
public let storyItem: EngineStoryItem?
|
||||
public let chatLocation: ChatLocation?
|
||||
|
||||
public init(
|
||||
externalState: ExternalState,
|
||||
@@ -124,10 +153,11 @@ public final class MessageInputPanelComponent: Component {
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
style: Style,
|
||||
placeholder: String,
|
||||
placeholder: Placeholder,
|
||||
maxLength: Int?,
|
||||
queryTypes: ContextQueryTypes,
|
||||
alwaysDarkWhenHasText: Bool,
|
||||
resetInputContents: SendMessageInput?,
|
||||
nextInputMode: @escaping (Bool) -> InputMode?,
|
||||
areVoiceMessagesAvailable: Bool,
|
||||
presentController: @escaping (ViewController) -> Void,
|
||||
@@ -144,7 +174,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
likeAction: (() -> Void)?,
|
||||
likeOptionsAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
inputModeAction: (() -> Void)?,
|
||||
timeoutAction: ((UIView) -> Void)?,
|
||||
timeoutAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
forwardAction: (() -> Void)?,
|
||||
moreAction: ((UIView, ContextGesture?) -> Void)?,
|
||||
presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?,
|
||||
@@ -165,7 +195,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
hideKeyboard: Bool,
|
||||
forceIsEditing: Bool,
|
||||
disabledPlaceholder: String?,
|
||||
storyId: Int32?
|
||||
isChannel: Bool,
|
||||
storyItem: EngineStoryItem?,
|
||||
chatLocation: ChatLocation?
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@@ -177,6 +209,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.maxLength = maxLength
|
||||
self.queryTypes = queryTypes
|
||||
self.alwaysDarkWhenHasText = alwaysDarkWhenHasText
|
||||
self.resetInputContents = resetInputContents
|
||||
self.areVoiceMessagesAvailable = areVoiceMessagesAvailable
|
||||
self.presentController = presentController
|
||||
self.presentInGlobalOverlay = presentInGlobalOverlay
|
||||
@@ -213,7 +246,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.hideKeyboard = hideKeyboard
|
||||
self.forceIsEditing = forceIsEditing
|
||||
self.disabledPlaceholder = disabledPlaceholder
|
||||
self.storyId = storyId
|
||||
self.isChannel = isChannel
|
||||
self.storyItem = storyItem
|
||||
self.chatLocation = chatLocation
|
||||
}
|
||||
|
||||
public static func ==(lhs: MessageInputPanelComponent, rhs: MessageInputPanelComponent) -> Bool {
|
||||
@@ -244,6 +279,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
if lhs.alwaysDarkWhenHasText != rhs.alwaysDarkWhenHasText {
|
||||
return false
|
||||
}
|
||||
if lhs.resetInputContents != rhs.resetInputContents {
|
||||
return false
|
||||
}
|
||||
if lhs.areVoiceMessagesAvailable != rhs.areVoiceMessagesAvailable {
|
||||
return false
|
||||
}
|
||||
@@ -307,13 +345,19 @@ public final class MessageInputPanelComponent: Component {
|
||||
if (lhs.likeOptionsAction == nil) != (rhs.likeOptionsAction == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.storyId != rhs.storyId {
|
||||
if lhs.isChannel != rhs.isChannel {
|
||||
return false
|
||||
}
|
||||
if lhs.storyItem != rhs.storyItem {
|
||||
return false
|
||||
}
|
||||
if lhs.chatLocation != rhs.chatLocation {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public enum SendMessageInput {
|
||||
public enum SendMessageInput: Equatable {
|
||||
case text(NSAttributedString)
|
||||
}
|
||||
|
||||
@@ -329,6 +373,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
private let counter = ComponentView<Empty>()
|
||||
|
||||
private var disabledPlaceholder: ComponentView<Empty>?
|
||||
private var textClippingView = UIView()
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
|
||||
@@ -360,11 +405,17 @@ public final class MessageInputPanelComponent: Component {
|
||||
private var viewForOverlayContent: ViewForOverlayContent?
|
||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||
|
||||
private var viewsIconView: UIImageView?
|
||||
private var viewStatsCountText: AnimatedCountLabelView?
|
||||
private var reactionStatsCountText: AnimatedCountLabelView?
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
private var component: MessageInputPanelComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private var pendingSetMessageInput: SendMessageInput?
|
||||
|
||||
public var likeButtonView: UIView? {
|
||||
return self.likeButton.view
|
||||
}
|
||||
@@ -380,6 +431,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
let blurEffect = UIBlurEffect(style: style)
|
||||
let vibrancyEffect = UIVibrancyEffect(blurEffect: blurEffect)
|
||||
let vibrancyEffectView = UIVisualEffectView(effect: vibrancyEffect)
|
||||
vibrancyEffectView.alpha = 0.0
|
||||
self.vibrancyEffectView = vibrancyEffectView
|
||||
|
||||
self.mediaRecordingVibrancyContainer = UIView()
|
||||
@@ -388,12 +440,15 @@ public final class MessageInputPanelComponent: Component {
|
||||
self.gradientView = UIImageView()
|
||||
self.bottomGradientView = UIView()
|
||||
|
||||
self.textClippingView.clipsToBounds = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.bottomGradientView)
|
||||
self.addSubview(self.gradientView)
|
||||
self.fieldBackgroundView.addSubview(self.vibrancyEffectView)
|
||||
self.addSubview(self.fieldBackgroundView)
|
||||
self.addSubview(self.textClippingView)
|
||||
|
||||
self.viewForOverlayContent = ViewForOverlayContent(
|
||||
ignoreHit: { [weak self] view, point in
|
||||
@@ -422,6 +477,14 @@ public final class MessageInputPanelComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
public func hasFirstResponder() -> Bool {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
return textFieldView.hasFirstResponder()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func getSendMessageInput() -> SendMessageInput {
|
||||
guard let textFieldView = self.textField.view as? TextFieldComponent.View else {
|
||||
return .text(NSAttributedString())
|
||||
@@ -430,6 +493,17 @@ public final class MessageInputPanelComponent: Component {
|
||||
return .text(textFieldView.getAttributedText())
|
||||
}
|
||||
|
||||
public func setSendMessageInput(value: SendMessageInput, updateState: Bool) {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
switch value {
|
||||
case let .text(text):
|
||||
textFieldView.setAttributedText(text, updateState: updateState)
|
||||
}
|
||||
} else {
|
||||
self.pendingSetMessageInput = value
|
||||
}
|
||||
}
|
||||
|
||||
public func getAttachmentButtonView() -> UIView? {
|
||||
guard let attachmentButtonView = self.attachmentButton.view else {
|
||||
return nil
|
||||
@@ -437,9 +511,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
return attachmentButtonView
|
||||
}
|
||||
|
||||
public func clearSendMessageInput() {
|
||||
public func clearSendMessageInput(updateState: Bool) {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.setAttributedText(NSAttributedString())
|
||||
textFieldView.setAttributedText(NSAttributedString(), updateState: updateState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,7 +572,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if component.queryTypes.contains(.emoji) {
|
||||
availableTypes.append(.emoji)
|
||||
}
|
||||
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, currentQueryStates: &self.contextQueryStates)
|
||||
let contextQueryUpdates = contextQueryResultState(context: context, inputState: inputState, availableTypes: availableTypes, chatLocation: component.chatLocation, currentQueryStates: &self.contextQueryStates)
|
||||
|
||||
for (kind, update) in contextQueryUpdates {
|
||||
switch update {
|
||||
@@ -560,6 +634,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousPlaceholder = self.component?.placeholder
|
||||
|
||||
var insets = UIEdgeInsets(top: 14.0, left: 9.0, bottom: 6.0, right: 41.0)
|
||||
|
||||
if let _ = component.attachmentAction {
|
||||
@@ -569,9 +645,19 @@ public final class MessageInputPanelComponent: Component {
|
||||
insets.right = 41.0
|
||||
}
|
||||
|
||||
let mediaInsets = UIEdgeInsets(top: insets.top, left: 9.0, bottom: insets.bottom, right: 41.0)
|
||||
var textFieldSideInset = 9.0
|
||||
if case .media = component.style {
|
||||
textFieldSideInset = 8.0
|
||||
}
|
||||
|
||||
let mediaInsets = UIEdgeInsets(top: insets.top, left: textFieldSideInset, bottom: insets.bottom, right: 41.0)
|
||||
|
||||
let baseFieldHeight: CGFloat = 40.0
|
||||
|
||||
var transition = transition
|
||||
if transition.animation.isImmediate, let previousComponent = self.component, previousComponent.storyItem?.id == component.storyItem?.id, component.isChannel {
|
||||
transition = transition.withAnimation(.curve(duration: 0.3, curve: .spring))
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
@@ -626,6 +712,13 @@ public final class MessageInputPanelComponent: Component {
|
||||
textColor: UIColor(rgb: 0xffffff),
|
||||
insets: UIEdgeInsets(top: 9.0, left: 8.0, bottom: 10.0, right: 48.0),
|
||||
hideKeyboard: component.hideKeyboard,
|
||||
resetText: component.resetInputContents.flatMap { resetInputContents in
|
||||
switch resetInputContents {
|
||||
case let .text(value):
|
||||
return value
|
||||
}
|
||||
},
|
||||
isOneLineWhenUnfocused: component.style == .media,
|
||||
formatMenuAvailability: component.isFormattingLocked ? .locked : .available,
|
||||
lockedFormatAction: {
|
||||
component.presentTextFormattingTooltip?()
|
||||
@@ -642,23 +735,39 @@ public final class MessageInputPanelComponent: Component {
|
||||
)
|
||||
let isEditing = self.textFieldExternalState.isEditing || component.forceIsEditing
|
||||
|
||||
var placeholderItems: [AnimatedTextComponent.Item] = []
|
||||
switch component.placeholder {
|
||||
case let .plain(string):
|
||||
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(0 as Int), content: .text(string)))
|
||||
case let .counter(items):
|
||||
for item in items {
|
||||
switch item.content {
|
||||
case let .text(string):
|
||||
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .text(string)))
|
||||
case let .number(value, minDigits):
|
||||
placeholderItems.append(AnimatedTextComponent.Item(id: AnyHashable(item.id), content: .number(value, minDigits: minDigits)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let placeholderTransition: Transition = (previousPlaceholder != nil && previousPlaceholder != component.placeholder) ? Transition(animation: .curve(duration: 0.3, curve: .spring)) : .immediate
|
||||
let placeholderSize = self.placeholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.placeholder,
|
||||
transition: placeholderTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: Font.regular(17.0),
|
||||
color: UIColor(rgb: 0xffffff, alpha: 0.3)
|
||||
color: UIColor(rgb: 0xffffff, alpha: 0.3),
|
||||
items: placeholderItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
|
||||
let _ = self.vibrancyPlaceholder.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: component.placeholder,
|
||||
transition: placeholderTransition,
|
||||
component: AnyComponent(AnimatedTextComponent(
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
color: .white,
|
||||
items: placeholderItems
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
@@ -672,20 +781,33 @@ public final class MessageInputPanelComponent: Component {
|
||||
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 if isEditing {
|
||||
} else if isEditing || component.style == .editor || component.style == .media {
|
||||
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 component.likeAction != nil && component.forwardAction != nil {
|
||||
fieldBackgroundFrame.size.width -= 49.0
|
||||
if component.forwardAction != nil && component.likeAction != nil {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right - 49.0, height: textFieldSize.height))
|
||||
} else if component.forwardAction != nil {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - insets.right, height: textFieldSize.height))
|
||||
} else {
|
||||
fieldBackgroundFrame = CGRect(origin: CGPoint(x: mediaInsets.left, y: insets.top), size: CGSize(width: availableSize.width - mediaInsets.left - 50.0, height: textFieldSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.vibrancyEffectView, frame: CGRect(origin: CGPoint(), size: fieldBackgroundFrame.size))
|
||||
self.vibrancyEffectView.isHidden = component.style == .media
|
||||
if isEditing {
|
||||
self.vibrancyEffectView.alpha = 1.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.fieldBackgroundView, frame: fieldBackgroundFrame)
|
||||
self.fieldBackgroundView.update(size: fieldBackgroundFrame.size, cornerRadius: baseFieldHeight * 0.5, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
var textClippingFrame = fieldBackgroundFrame
|
||||
if component.style == .media, !isEditing {
|
||||
textClippingFrame.size.height -= 10.0
|
||||
}
|
||||
transition.setFrame(view: self.textClippingView, frame: textClippingFrame)
|
||||
|
||||
let gradientFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX - fieldFrame.minX, y: -topGradientHeight), size: CGSize(width: availableSize.width - (fieldBackgroundFrame.minX - fieldFrame.minX), height: topGradientHeight + fieldBackgroundFrame.maxY + insets.bottom))
|
||||
transition.setFrame(view: self.gradientView, frame: gradientFrame)
|
||||
transition.setFrame(view: self.bottomGradientView, frame: CGRect(origin: CGPoint(x: 0.0, y: gradientFrame.maxY), size: CGSize(width: availableSize.width, height: component.bottomInset)))
|
||||
@@ -715,32 +837,120 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
|
||||
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
|
||||
transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: placeholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: vibrancyPlaceholderView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
transition.setAlpha(view: self.fieldBackgroundView, alpha: component.disabledPlaceholder != nil ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: self.fieldBackgroundView, alpha: (component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
|
||||
|
||||
let size = CGSize(width: availableSize.width, height: textFieldSize.height + insets.top + insets.bottom)
|
||||
|
||||
if let textFieldView = self.textField.view {
|
||||
var rightButtonsOffsetX: CGFloat = 0.0
|
||||
if component.isChannel, let storyItem = component.storyItem {
|
||||
var viewsTransition = transition
|
||||
|
||||
let viewsIconView: UIImageView
|
||||
if let current = self.viewsIconView {
|
||||
viewsIconView = current
|
||||
} else {
|
||||
viewsTransition = viewsTransition.withAnimation(.none)
|
||||
viewsIconView = UIImageView(image: UIImage(bundleImageName: "Stories/EmbeddedViewIcon"))
|
||||
self.viewsIconView = viewsIconView
|
||||
self.addSubview(viewsIconView)
|
||||
}
|
||||
|
||||
let viewStatsCountText: AnimatedCountLabelView
|
||||
if let current = self.viewStatsCountText {
|
||||
viewStatsCountText = current
|
||||
} else {
|
||||
viewStatsCountText = AnimatedCountLabelView(frame: CGRect())
|
||||
self.viewStatsCountText = viewStatsCountText
|
||||
self.addSubview(viewStatsCountText)
|
||||
}
|
||||
|
||||
let reactionStatsCountText: AnimatedCountLabelView
|
||||
if let current = self.reactionStatsCountText {
|
||||
reactionStatsCountText = current
|
||||
} else {
|
||||
reactionStatsCountText = AnimatedCountLabelView(frame: CGRect())
|
||||
self.reactionStatsCountText = reactionStatsCountText
|
||||
self.addSubview(reactionStatsCountText)
|
||||
}
|
||||
|
||||
var viewCount = storyItem.views?.seenCount ?? 0
|
||||
if viewCount == 0 {
|
||||
viewCount = 1
|
||||
}
|
||||
var reactionCount = storyItem.views?.reactedCount ?? 0
|
||||
if reactionCount == 0, storyItem.myReaction != nil {
|
||||
reactionCount += 1
|
||||
}
|
||||
|
||||
var regularSegments: [AnimatedCountLabelView.Segment] = []
|
||||
regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)))
|
||||
|
||||
var reactionSegments: [AnimatedCountLabelView.Segment] = []
|
||||
reactionSegments.append(.number(reactionCount, NSAttributedString(string: "\(reactionCount)", font: Font.with(size: 15.0, traits: .monospacedNumbers), textColor: .white)))
|
||||
|
||||
let viewStatsTextLayout = viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: viewsTransition.containedViewLayoutTransition)
|
||||
let reactionStatsTextLayout = reactionStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: reactionSegments, transition: viewsTransition.containedViewLayoutTransition)
|
||||
|
||||
var contentX: CGFloat = 16.0
|
||||
|
||||
if let image = viewsIconView.image {
|
||||
let viewsIconFrame = CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - image.size.height) * 0.5)), size: image.size)
|
||||
viewsTransition.setPosition(view: viewsIconView, position: viewsIconFrame.center)
|
||||
viewsTransition.setBounds(view: viewsIconView, bounds: CGRect(origin: CGPoint(), size: viewsIconFrame.size))
|
||||
|
||||
contentX += image.size.width + 5.0
|
||||
}
|
||||
|
||||
transition.setFrame(view: viewStatsCountText, frame: CGRect(origin: CGPoint(x: contentX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - viewStatsTextLayout.size.height) * 0.5)), size: viewStatsTextLayout.size))
|
||||
|
||||
transition.setFrame(view: reactionStatsCountText, frame: CGRect(origin: CGPoint(x: availableSize.width - 11.0 - reactionStatsTextLayout.size.width, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - reactionStatsTextLayout.size.height) * 0.5)), size: reactionStatsTextLayout.size))
|
||||
|
||||
rightButtonsOffsetX -= reactionStatsTextLayout.size.width + 4.0
|
||||
} else {
|
||||
if let viewsIconView = self.viewsIconView {
|
||||
self.viewsIconView = nil
|
||||
viewsIconView.removeFromSuperview()
|
||||
}
|
||||
if let viewStatsCountText = self.viewStatsCountText {
|
||||
self.viewStatsCountText = nil
|
||||
viewStatsCountText.removeFromSuperview()
|
||||
}
|
||||
if let reactionStatsCountText = self.reactionStatsCountText {
|
||||
self.reactionStatsCountText = nil
|
||||
reactionStatsCountText.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
if textFieldView.superview == nil {
|
||||
self.addSubview(textFieldView)
|
||||
self.textClippingView.addSubview(textFieldView)
|
||||
|
||||
if let viewForOverlayContent = self.viewForOverlayContent {
|
||||
self.addSubview(viewForOverlayContent)
|
||||
}
|
||||
|
||||
if let pendingSetMessageInput = self.pendingSetMessageInput {
|
||||
self.pendingSetMessageInput = nil
|
||||
switch pendingSetMessageInput {
|
||||
case let .text(text):
|
||||
textFieldView.setAttributedText(text, updateState: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: fieldBackgroundFrame.minX, y: fieldBackgroundFrame.maxY - textFieldSize.height), size: textFieldSize)
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: textFieldSize)
|
||||
transition.setFrame(view: textFieldView, frame: textFieldFrame)
|
||||
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil) ? 0.0 : 1.0)
|
||||
transition.setAlpha(view: textFieldView, alpha: (hasMediaRecording || hasMediaEditing || component.disabledPlaceholder != nil || component.isChannel) ? 0.0 : 1.0)
|
||||
|
||||
if let viewForOverlayContent = self.viewForOverlayContent {
|
||||
transition.setFrame(view: viewForOverlayContent, frame: textFieldFrame)
|
||||
}
|
||||
}
|
||||
|
||||
if let disabledPlaceholderText = component.disabledPlaceholder {
|
||||
if let disabledPlaceholderText = component.disabledPlaceholder, !component.isChannel {
|
||||
let disabledPlaceholder: ComponentView<Empty>
|
||||
var disabledPlaceholderTransition = transition
|
||||
if let current = self.disabledPlaceholder {
|
||||
@@ -809,7 +1019,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputActionButtonComponent(
|
||||
mode: attachmentButtonMode,
|
||||
storyId: component.storyId,
|
||||
storyId: component.storyItem?.id,
|
||||
action: { [weak self] mode, action, sendAction in
|
||||
guard let self, let component = self.component, case .up = action else {
|
||||
return
|
||||
@@ -937,6 +1147,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
let inputActionButtonMode: MessageInputActionButtonComponent.Mode
|
||||
if case .editor = component.style {
|
||||
inputActionButtonMode = isEditing ? .apply : .none
|
||||
} else if case .media = component.style {
|
||||
inputActionButtonMode = isEditing ? .apply : .none
|
||||
} else {
|
||||
if hasMediaEditing {
|
||||
inputActionButtonMode = .send
|
||||
@@ -958,7 +1170,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputActionButtonComponent(
|
||||
mode: inputActionButtonMode,
|
||||
storyId: component.storyId,
|
||||
storyId: component.storyItem?.id,
|
||||
action: { [weak self] mode, action, sendAction in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@@ -1006,7 +1218,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
break
|
||||
}
|
||||
},
|
||||
longPressAction: component.sendMessageOptionsAction,
|
||||
longPressAction: inputActionButtonMode == .send ? component.sendMessageOptionsAction : nil,
|
||||
switchMediaInputMode: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
@@ -1067,14 +1279,24 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
var inputActionButtonOriginX: CGFloat
|
||||
if component.setMediaRecordingActive != nil || isEditing {
|
||||
inputActionButtonOriginX = fieldBackgroundFrame.maxX + floorToScreenPixels((41.0 - inputActionButtonSize.width) * 0.5)
|
||||
if rightButtonsOffsetX != 0.0 {
|
||||
inputActionButtonOriginX = availableSize.width - 3.0 + rightButtonsOffsetX
|
||||
if displayLikeAction {
|
||||
inputActionButtonOriginX -= 39.0
|
||||
}
|
||||
if component.forwardAction != nil {
|
||||
inputActionButtonOriginX -= 46.0
|
||||
}
|
||||
} else {
|
||||
inputActionButtonOriginX = size.width
|
||||
}
|
||||
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 3.0
|
||||
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 {
|
||||
@@ -1086,8 +1308,14 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition.setBounds(view: inputActionButtonView, bounds: CGRect(origin: CGPoint(), size: inputActionButtonFrame.size))
|
||||
transition.setAlpha(view: inputActionButtonView, alpha: likeActionReplacesInputAction ? 0.0 : 1.0)
|
||||
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 41.0
|
||||
if rightButtonsOffsetX != 0.0 {
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 46.0
|
||||
}
|
||||
} else {
|
||||
if hasLikeAction {
|
||||
inputActionButtonOriginX += 41.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1095,7 +1323,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
transition: transition,
|
||||
component: AnyComponent(MessageInputActionButtonComponent(
|
||||
mode: .like(reaction: component.myReaction?.reaction, file: component.myReaction?.file, animationFileId: component.myReaction?.animationFileId),
|
||||
storyId: component.storyId,
|
||||
storyId: component.storyItem?.id,
|
||||
action: { [weak self] _, action, _ in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
@@ -1129,7 +1357,10 @@ public final class MessageInputPanelComponent: Component {
|
||||
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)
|
||||
var likeButtonFrame = CGRect(origin: CGPoint(x: inputActionButtonOriginX, y: size.height - insets.bottom - baseFieldHeight + floor((baseFieldHeight - likeButtonSize.height) * 0.5)), size: likeButtonSize)
|
||||
if component.forwardAction == nil && rightButtonsOffsetX == 0.0 {
|
||||
likeButtonFrame.origin.x += 3.0
|
||||
}
|
||||
transition.setPosition(view: likeButtonView, position: likeButtonFrame.center)
|
||||
transition.setBounds(view: likeButtonView, bounds: CGRect(origin: CGPoint(), size: likeButtonFrame.size))
|
||||
transition.setAlpha(view: likeButtonView, alpha: displayLikeAction ? 1.0 : 0.0)
|
||||
@@ -1239,45 +1470,25 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let accentColor = component.theme.chat.inputPanel.panelControlAccentColor
|
||||
if let timeoutAction = component.timeoutAction, let timeoutValue = component.timeoutValue {
|
||||
func generateIcon(value: String) -> UIImage? {
|
||||
let image = UIImage(bundleImageName: "Media Editor/Timeout")!
|
||||
let valueString = NSAttributedString(string: value, font: Font.with(size: value.count == 1 ? 12.0 : 10.0, design: .round, weight: .semibold), textColor: .white, paragraphAlignment: .center)
|
||||
|
||||
return generateImage(image.size, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: CGPoint(), size: size)
|
||||
context.clear(bounds)
|
||||
|
||||
if let cgImage = image.cgImage {
|
||||
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
|
||||
var offset: CGPoint = CGPoint(x: 0.0, y: -3.0 - UIScreenPixel)
|
||||
if value == "∞" {
|
||||
offset.x += UIScreenPixel
|
||||
offset.y += 1.0 - UIScreenPixel
|
||||
}
|
||||
|
||||
let valuePath = CGMutablePath()
|
||||
valuePath.addRect(bounds.offsetBy(dx: offset.x, dy: offset.y))
|
||||
let valueFramesetter = CTFramesetterCreateWithAttributedString(valueString as CFAttributedString)
|
||||
let valyeFrame = CTFramesetterCreateFrame(valueFramesetter, CFRangeMake(0, valueString.length), valuePath, nil)
|
||||
CTFrameDraw(valyeFrame, context)
|
||||
})?.withRenderingMode(.alwaysTemplate)
|
||||
}
|
||||
|
||||
let icon = generateIcon(value: timeoutValue)
|
||||
let timeoutButtonSize = self.timeoutButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Image(image: icon, tintColor: component.timeoutSelected ? UIColor(rgb: 0xf8d74a) : UIColor(white: 1.0, alpha: 1.0), size: CGSize(width: 20.0, height: 20.0))),
|
||||
action: { [weak self] in
|
||||
guard let self, let timeoutButtonView = self.timeoutButton.view else {
|
||||
return
|
||||
}
|
||||
timeoutAction(timeoutButtonView)
|
||||
component: AnyComponent(ContextReferenceButtonComponent(
|
||||
content: AnyComponent(
|
||||
TimeoutContentComponent(
|
||||
color: .white,
|
||||
accentColor: accentColor,
|
||||
isSelected: component.timeoutSelected,
|
||||
value: timeoutValue
|
||||
)
|
||||
),
|
||||
tag: timeoutButtonTag,
|
||||
minSize: CGSize(width: 32.0, height: 32.0),
|
||||
action: { view, gesture in
|
||||
timeoutAction(view, gesture)
|
||||
}
|
||||
).minSize(CGSize(width: 32.0, height: 32.0))),
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
@@ -1296,7 +1507,9 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
var fieldBackgroundIsDark = false
|
||||
if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||
if component.style == .media {
|
||||
|
||||
} else if self.textFieldExternalState.hasText && component.alwaysDarkWhenHasText {
|
||||
fieldBackgroundIsDark = true
|
||||
} else if isEditing || component.style == .editor {
|
||||
fieldBackgroundIsDark = true
|
||||
@@ -1468,8 +1681,12 @@ public final class MessageInputPanelComponent: Component {
|
||||
|
||||
self.updateContextQueries()
|
||||
|
||||
let panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||
let panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||
var panelLeftInset: CGFloat = max(insets.left, 7.0)
|
||||
var panelRightInset: CGFloat = max(insets.right, 41.0)
|
||||
if case .media = component.style {
|
||||
panelLeftInset = 0.0
|
||||
panelRightInset = 0.0
|
||||
}
|
||||
|
||||
var contextResults: ContextResultPanelComponent.Results?
|
||||
if let result = self.contextQueryResults[.mention], case let .mentions(mentions) = result, !mentions.isEmpty {
|
||||
@@ -1601,7 +1818,13 @@ public final class MessageInputPanelComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - panelLeftInset - panelRightInset, height: availablePanelHeight)
|
||||
)
|
||||
|
||||
let panelFrame = CGRect(origin: CGPoint(x: insets.left, y: -panelSize.height + 14.0), size: CGSize(width: panelSize.width, height: panelSize.height + 19.0))
|
||||
var panelOriginY = -panelSize.height + 14.0
|
||||
var panelHeight = panelSize.height + 19.0
|
||||
if case .media = component.style {
|
||||
panelOriginY -= 6.0
|
||||
panelHeight = panelSize.height
|
||||
}
|
||||
let panelFrame = CGRect(origin: CGPoint(x: panelLeftInset, y: panelOriginY), size: CGSize(width: panelSize.width, height: panelHeight))
|
||||
if let panelView = panel.view as? ContextResultPanelComponent.View {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, at: 0)
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
|
||||
public final class TimeoutContentComponent: Component {
|
||||
public let color: UIColor
|
||||
public let accentColor: UIColor
|
||||
public let isSelected: Bool
|
||||
public let value: String
|
||||
|
||||
public init(
|
||||
color: UIColor,
|
||||
accentColor: UIColor,
|
||||
isSelected: Bool,
|
||||
value: String
|
||||
) {
|
||||
self.color = color
|
||||
self.accentColor = accentColor
|
||||
self.isSelected = isSelected
|
||||
self.value = value
|
||||
}
|
||||
|
||||
public static func ==(lhs: TimeoutContentComponent, rhs: TimeoutContentComponent) -> Bool {
|
||||
if lhs.color != rhs.color {
|
||||
return false
|
||||
}
|
||||
if lhs.accentColor != rhs.accentColor {
|
||||
return false
|
||||
}
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private var component: TimeoutContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private let background: UIImageView
|
||||
private let foreground: UIImageView
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.background = UIImageView(image: UIImage(bundleImageName: "Media Editor/Timeout"))
|
||||
self.foreground = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.background)
|
||||
self.addSubview(self.foreground)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: TimeoutContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let size = CGSize(width: 20.0, height: 20.0)
|
||||
if previousComponent?.accentColor != component.accentColor {
|
||||
self.foreground.image = generateFilledCircleImage(diameter: size.width, color: component.accentColor)
|
||||
}
|
||||
|
||||
var updated = false
|
||||
if let previousComponent {
|
||||
if previousComponent.isSelected != component.isSelected {
|
||||
updated = true
|
||||
}
|
||||
if previousComponent.value != component.value {
|
||||
if let textView = self.text.view, let snapshotView = textView.snapshotView(afterScreenUpdates: false) {
|
||||
snapshotView.frame = textView.frame
|
||||
self.addSubview(snapshotView)
|
||||
snapshotView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: -3.0), duration: 0.2, removeOnCompletion: false, additive: true)
|
||||
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
snapshotView.removeFromSuperview()
|
||||
})
|
||||
|
||||
textView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
textView.layer.animatePosition(from: CGPoint(x: 0.0, y: 3.0), to: .zero, duration: 0.2, additive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let fontSize: CGFloat
|
||||
let textOffset: CGFloat
|
||||
if component.value.count == 1 {
|
||||
fontSize = 12.0
|
||||
textOffset = UIScreenPixel
|
||||
} else {
|
||||
fontSize = 10.0
|
||||
textOffset = -UIScreenPixel
|
||||
}
|
||||
|
||||
let font = Font.with(size: fontSize, design: .round, weight: .semibold)
|
||||
let textSize = self.text.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: component.value, font: font, color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 100.0, height: 100.0)
|
||||
)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
let textFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - textSize.width) / 2.0) + UIScreenPixel, y: floorToScreenPixels((size.height - textSize.height) / 2.0) + textOffset), size: textSize)
|
||||
transition.setPosition(view: textView, position: textFrame.center)
|
||||
textView.bounds = CGRect(origin: CGPoint(), size: textFrame.size)
|
||||
}
|
||||
|
||||
self.background.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
self.foreground.bounds = CGRect(origin: .zero, size: size)
|
||||
self.foreground.center = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
|
||||
|
||||
let foregroundTransition: Transition = updated ? .easeInOut(duration: 0.2) : transition
|
||||
foregroundTransition.setScale(view: self.foreground, scale: component.isSelected ? 1.0 : 0.001)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user