mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 21:45:19 +00:00
- custom poll emoji input
- avatars in forward info - ban user sheet
This commit is contained in:
parent
a9c8ae8595
commit
5123b841c3
@ -159,14 +159,16 @@ public struct ChatAvailableMessageActionOptions: OptionSet {
|
||||
public struct ChatAvailableMessageActions {
|
||||
public var options: ChatAvailableMessageActionOptions
|
||||
public var banAuthor: Peer?
|
||||
public var banAuthors: [Peer]
|
||||
public var disableDelete: Bool
|
||||
public var isCopyProtected: Bool
|
||||
public var setTag: Bool
|
||||
public var editTags: Set<MessageReaction.Reaction>
|
||||
|
||||
public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, disableDelete: Bool, isCopyProtected: Bool, setTag: Bool, editTags: Set<MessageReaction.Reaction>) {
|
||||
public init(options: ChatAvailableMessageActionOptions, banAuthor: Peer?, banAuthors: [Peer], disableDelete: Bool, isCopyProtected: Bool, setTag: Bool, editTags: Set<MessageReaction.Reaction>) {
|
||||
self.options = options
|
||||
self.banAuthor = banAuthor
|
||||
self.banAuthors = banAuthors
|
||||
self.disableDelete = disableDelete
|
||||
self.isCopyProtected = isCopyProtected
|
||||
self.setTag = setTag
|
||||
|
@ -68,6 +68,7 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
|
||||
var isPanningUpdated: (Bool) -> Void = { _ in }
|
||||
var isExpandedUpdated: (Bool) -> Void = { _ in }
|
||||
var isPanGestureEnabled: (() -> Bool)?
|
||||
var onExpandAnimationCompleted: () -> Void = {}
|
||||
|
||||
override init() {
|
||||
@ -132,6 +133,12 @@ final class AttachmentContainer: ASDisplayNode, ASGestureRecognizerDelegate {
|
||||
if case .regular = layout.metrics.widthClass {
|
||||
return false
|
||||
}
|
||||
|
||||
if let isPanGestureEnabled = self.isPanGestureEnabled {
|
||||
if !isPanGestureEnabled() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -93,6 +93,7 @@ public protocol AttachmentContainable: ViewController {
|
||||
var cancelPanGesture: () -> Void { get set }
|
||||
var isContainerPanning: () -> Bool { get set }
|
||||
var isContainerExpanded: () -> Bool { get set }
|
||||
var isPanGestureEnabled: (() -> Bool)? { get }
|
||||
var mediaPickerContext: AttachmentMediaPickerContext? { get }
|
||||
|
||||
func isContainerPanningUpdated(_ panning: Bool)
|
||||
@ -124,6 +125,10 @@ public extension AttachmentContainable {
|
||||
func shouldDismissImmediately() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
var isPanGestureEnabled: (() -> Bool)? {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public enum AttachmentMediaPickerSendMode {
|
||||
@ -351,6 +356,17 @@ public class AttachmentController: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
self.container.isPanGestureEnabled = { [weak self] in
|
||||
guard let self, let currentController = self.currentControllers.last else {
|
||||
return true
|
||||
}
|
||||
if let isPanGestureEnabled = currentController.isPanGestureEnabled {
|
||||
return isPanGestureEnabled()
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
self.container.shouldCancelPanGesture = { [weak self] in
|
||||
if let strongSelf = self, let currentController = strongSelf.currentControllers.last {
|
||||
if !currentController.shouldDismissImmediately() {
|
||||
@ -548,6 +564,7 @@ public class AttachmentController: ViewController {
|
||||
strongSelf.panel.updateBackgroundAlpha(alpha, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
controller.cancelPanGesture = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
strongSelf.container.cancelPanGesture()
|
||||
|
@ -37,6 +37,10 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/ListMultilineTextFieldItemComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/TextFieldComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/ChatPresentationInterfaceState",
|
||||
"//submodules/TelegramUI/Components/EmojiSuggestionsComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -21,6 +21,11 @@ import PeerAllowedReactionsScreen
|
||||
import AttachmentUI
|
||||
import ListMultilineTextFieldItemComponent
|
||||
import ListActionItemComponent
|
||||
import ChatEntityKeyboardInputNode
|
||||
import ChatPresentationInterfaceState
|
||||
import EmojiSuggestionsComponent
|
||||
import TextFormat
|
||||
import TextFieldComponent
|
||||
|
||||
final class ComposePollScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -48,7 +53,7 @@ final class ComposePollScreenComponent: Component {
|
||||
|
||||
private final class PollOption {
|
||||
let id: Int
|
||||
let textInputState = ListComposePollOptionComponent.ExternalState()
|
||||
let textInputState = TextFieldComponent.ExternalState()
|
||||
let textFieldTag = NSObject()
|
||||
var resetText: String?
|
||||
|
||||
@ -78,10 +83,7 @@ final class ComposePollScreenComponent: Component {
|
||||
private(set) weak var state: EmptyComponentState?
|
||||
private var environment: EnvironmentType?
|
||||
|
||||
private var emojiContent: EmojiPagerContentComponent?
|
||||
private var emojiContentDisposable: Disposable?
|
||||
|
||||
private let pollTextInputState = ListMultilineTextFieldItemComponent.ExternalState()
|
||||
private let pollTextInputState = TextFieldComponent.ExternalState()
|
||||
private let pollTextFieldTag = NSObject()
|
||||
private var resetPollText: String?
|
||||
|
||||
@ -96,7 +98,18 @@ final class ComposePollScreenComponent: Component {
|
||||
private var isQuiz: Bool = false
|
||||
private var selectedQuizOptionId: Int?
|
||||
|
||||
private var displayInput: Bool = false
|
||||
private var currentInputMode: ListComposePollOptionComponent.InputMode = .keyboard
|
||||
|
||||
private var inputMediaNodeData: ChatEntityKeyboardInputNode.InputData?
|
||||
private var inputMediaNodeDataDisposable: Disposable?
|
||||
private var inputMediaNodeStateContext = ChatEntityKeyboardInputNode.StateContext()
|
||||
private var inputMediaInteraction: ChatEntityKeyboardInputNode.Interaction?
|
||||
private var inputMediaNode: ChatEntityKeyboardInputNode?
|
||||
private var inputMediaNodeBackground = SimpleLayer()
|
||||
|
||||
private let inputMediaNodeDataPromise = Promise<ChatEntityKeyboardInputNode.InputData>()
|
||||
|
||||
private var currentEmojiSuggestionView: ComponentHostView<Empty>?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
@ -121,7 +134,7 @@ final class ComposePollScreenComponent: Component {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.emojiContentDisposable?.dispose()
|
||||
self.inputMediaNodeDataDisposable?.dispose()
|
||||
}
|
||||
|
||||
func scrollToTop() {
|
||||
@ -150,8 +163,19 @@ final class ComposePollScreenComponent: Component {
|
||||
if self.selectedQuizOptionId == pollOption.id {
|
||||
selectedQuizOption = optionData
|
||||
}
|
||||
var entities: [MessageTextEntity] = []
|
||||
for entity in generateChatInputTextEntities(pollOption.textInputState.text) {
|
||||
switch entity.type {
|
||||
case .CustomEmoji:
|
||||
entities.append(entity)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
mappedOptions.append(TelegramMediaPollOption(
|
||||
text: pollOption.textInputState.text.string,
|
||||
entities: entities,
|
||||
opaqueIdentifier: optionData
|
||||
))
|
||||
}
|
||||
@ -174,10 +198,20 @@ final class ComposePollScreenComponent: Component {
|
||||
mappedSolution = self.quizAnswerTextInputState.text.string
|
||||
}
|
||||
|
||||
var textEntities: [MessageTextEntity] = []
|
||||
for entity in generateChatInputTextEntities(self.pollTextInputState.text) {
|
||||
switch entity.type {
|
||||
case .CustomEmoji:
|
||||
textEntities.append(entity)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ComposedPoll(
|
||||
publicity: self.isAnonymous ? .anonymous : .public,
|
||||
kind: mappedKind,
|
||||
text: self.pollTextInputState.text.string,
|
||||
text: ComposedPoll.Text(string: self.pollTextInputState.text.string, entities: textEntities),
|
||||
options: mappedOptions,
|
||||
correctAnswers: mappedCorrectAnswers,
|
||||
results: TelegramMediaPollResults(
|
||||
@ -215,6 +249,187 @@ final class ComposePollScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func isPanGestureEnabled() -> Bool {
|
||||
if self.inputMediaNode != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
for (_, state) in self.collectTextInputStates() {
|
||||
if state.isEditing {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func updateInputMediaNode(
|
||||
component: ComposePollScreenComponent,
|
||||
availableSize: CGSize,
|
||||
bottomInset: CGFloat,
|
||||
inputHeight: CGFloat,
|
||||
effectiveInputHeight: CGFloat,
|
||||
metrics: LayoutMetrics,
|
||||
deviceMetrics: DeviceMetrics,
|
||||
transition: Transition
|
||||
) -> CGFloat {
|
||||
let bottomInset: CGFloat = bottomInset + 8.0
|
||||
let bottomContainerInset: CGFloat = 0.0
|
||||
let needsInputActivation: Bool = !"".isEmpty
|
||||
|
||||
var height: CGFloat = 0.0
|
||||
if case .emoji = self.currentInputMode, let inputData = self.inputMediaNodeData {
|
||||
let inputMediaNode: ChatEntityKeyboardInputNode
|
||||
var inputMediaNodeTransition = transition
|
||||
var animateIn = false
|
||||
if let current = self.inputMediaNode {
|
||||
inputMediaNode = current
|
||||
} else {
|
||||
animateIn = true
|
||||
inputMediaNodeTransition = inputMediaNodeTransition.withAnimation(.none)
|
||||
inputMediaNode = ChatEntityKeyboardInputNode(
|
||||
context: component.context,
|
||||
currentInputData: inputData,
|
||||
updatedInputData: self.inputMediaNodeDataPromise.get(),
|
||||
defaultToEmojiTab: true,
|
||||
opaqueTopPanelBackground: false,
|
||||
interaction: self.inputMediaInteraction,
|
||||
chatPeerId: nil,
|
||||
stateContext: self.inputMediaNodeStateContext
|
||||
)
|
||||
inputMediaNode.clipsToBounds = true
|
||||
|
||||
inputMediaNode.externalTopPanelContainerImpl = nil
|
||||
inputMediaNode.useExternalSearchContainer = true
|
||||
if inputMediaNode.view.superview == nil {
|
||||
self.inputMediaNodeBackground.removeAllAnimations()
|
||||
self.layer.addSublayer(self.inputMediaNodeBackground)
|
||||
self.addSubview(inputMediaNode.view)
|
||||
}
|
||||
self.inputMediaNode = inputMediaNode
|
||||
}
|
||||
|
||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let presentationInterfaceState = ChatPresentationInterfaceState(
|
||||
chatWallpaper: .builtin(WallpaperSettings()),
|
||||
theme: presentationData.theme,
|
||||
strings: presentationData.strings,
|
||||
dateTimeFormat: presentationData.dateTimeFormat,
|
||||
nameDisplayOrder: presentationData.nameDisplayOrder,
|
||||
limitsConfiguration: component.context.currentLimitsConfiguration.with { $0 },
|
||||
fontSize: presentationData.chatFontSize,
|
||||
bubbleCorners: presentationData.chatBubbleCorners,
|
||||
accountPeerId: component.context.account.peerId,
|
||||
mode: .standard(.default),
|
||||
chatLocation: .peer(id: component.context.account.peerId),
|
||||
subject: nil,
|
||||
peerNearbyData: nil,
|
||||
greetingData: nil,
|
||||
pendingUnpinnedAllMessages: false,
|
||||
activeGroupCallInfo: nil,
|
||||
hasActiveGroupCall: false,
|
||||
importState: nil,
|
||||
threadData: nil,
|
||||
isGeneralThreadClosed: nil,
|
||||
replyMessage: nil,
|
||||
accountPeerColor: nil,
|
||||
businessIntro: nil
|
||||
)
|
||||
|
||||
self.inputMediaNodeBackground.backgroundColor = presentationData.theme.rootController.navigationBar.opaqueBackgroundColor.cgColor
|
||||
|
||||
let heightAndOverflow = inputMediaNode.updateLayout(width: availableSize.width, leftInset: 0.0, rightInset: 0.0, bottomInset: bottomInset, standardInputHeight: deviceMetrics.standardInputHeight(inLandscape: false), inputHeight: inputHeight < 100.0 ? inputHeight - bottomContainerInset : inputHeight, maximumHeight: availableSize.height, inputPanelHeight: 0.0, transition: .immediate, interfaceState: presentationInterfaceState, layoutMetrics: metrics, deviceMetrics: deviceMetrics, isVisible: true, isExpanded: false)
|
||||
let inputNodeHeight = heightAndOverflow.0
|
||||
let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputNodeHeight), size: CGSize(width: availableSize.width, height: inputNodeHeight))
|
||||
|
||||
let inputNodeBackgroundFrame = CGRect(origin: CGPoint(x: inputNodeFrame.minX, y: inputNodeFrame.minY - 6.0), size: CGSize(width: inputNodeFrame.width, height: inputNodeFrame.height + 6.0))
|
||||
|
||||
if needsInputActivation {
|
||||
let inputNodeFrame = inputNodeFrame.offsetBy(dx: 0.0, dy: inputNodeHeight)
|
||||
Transition.immediate.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
Transition.immediate.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||
}
|
||||
|
||||
if animateIn {
|
||||
var targetFrame = inputMediaNode.frame
|
||||
targetFrame.origin.y = availableSize.height
|
||||
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: targetFrame)
|
||||
|
||||
let inputNodeBackgroundTargetFrame = CGRect(origin: CGPoint(x: targetFrame.minX, y: targetFrame.minY - 6.0), size: CGSize(width: targetFrame.width, height: targetFrame.height + 6.0))
|
||||
|
||||
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundTargetFrame)
|
||||
|
||||
transition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
transition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||
} else {
|
||||
inputMediaNodeTransition.setFrame(layer: inputMediaNode.layer, frame: inputNodeFrame)
|
||||
inputMediaNodeTransition.setFrame(layer: self.inputMediaNodeBackground, frame: inputNodeBackgroundFrame)
|
||||
}
|
||||
|
||||
height = heightAndOverflow.0
|
||||
} else if let inputMediaNode = self.inputMediaNode {
|
||||
self.inputMediaNode = nil
|
||||
|
||||
var targetFrame = inputMediaNode.frame
|
||||
targetFrame.origin.y = availableSize.height
|
||||
transition.setFrame(view: inputMediaNode.view, frame: targetFrame, completion: { [weak inputMediaNode] _ in
|
||||
if let inputMediaNode {
|
||||
Queue.mainQueue().after(0.3) {
|
||||
inputMediaNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak inputMediaNode] _ in
|
||||
inputMediaNode?.view.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
transition.setFrame(layer: self.inputMediaNodeBackground, frame: targetFrame, completion: { [weak self] _ in
|
||||
Queue.mainQueue().after(0.3) {
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.currentInputMode == .keyboard {
|
||||
self.inputMediaNodeBackground.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false, completion: { [weak self] finished in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if finished {
|
||||
self.inputMediaNodeBackground.removeFromSuperlayer()
|
||||
}
|
||||
self.inputMediaNodeBackground.removeAllAnimations()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/*if needsInputActivation {
|
||||
needsInputActivation = false
|
||||
Queue.mainQueue().justDispatch {
|
||||
inputPanelView.activateInput()
|
||||
}
|
||||
}*/
|
||||
|
||||
/*if let controller = self.environment?.controller() as? ComposePollScreen {
|
||||
controller.updateTabBarAlpha(self.inputMediaNode == nil ? 1.0 : 0.0, transition.containedViewLayoutTransition)
|
||||
}*/
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
private func collectTextInputStates() -> [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] {
|
||||
var textInputStates: [(view: ListComposePollOptionComponent.View, state: TextFieldComponent.ExternalState)] = []
|
||||
if let textInputView = self.pollTextSection.findTaggedView(tag: self.pollTextFieldTag) as? ListComposePollOptionComponent.View {
|
||||
textInputStates.append((textInputView, self.pollTextInputState))
|
||||
}
|
||||
for pollOption in self.pollOptions {
|
||||
if let textInputView = findTaggedComponentViewImpl(view: self.pollOptionsSectionContainer, tag: pollOption.textFieldTag) as? ListComposePollOptionComponent.View {
|
||||
textInputStates.append((textInputView, pollOption.textInputState))
|
||||
}
|
||||
}
|
||||
|
||||
return textInputStates
|
||||
}
|
||||
|
||||
func update(component: ComposePollScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -234,6 +449,104 @@ final class ComposePollScreenComponent: Component {
|
||||
id: self.nextPollOptionId
|
||||
))
|
||||
self.nextPollOptionId += 1
|
||||
|
||||
self.inputMediaNodeDataPromise.set(
|
||||
ChatEntityKeyboardInputNode.inputData(
|
||||
context: component.context,
|
||||
chatPeerId: nil,
|
||||
areCustomEmojiEnabled: true,
|
||||
hasTrending: false,
|
||||
hasSearch: true,
|
||||
hasStickers: false,
|
||||
hasGifs: false,
|
||||
hideBackground: true,
|
||||
sendGif: nil
|
||||
)
|
||||
)
|
||||
self.inputMediaNodeDataDisposable = (self.inputMediaNodeDataPromise.get()
|
||||
|> deliverOnMainQueue).start(next: { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.inputMediaNodeData = value
|
||||
})
|
||||
|
||||
self.inputMediaInteraction = ChatEntityKeyboardInputNode.Interaction(
|
||||
sendSticker: { _, _, _, _, _, _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
sendEmoji: { _, _, _ in
|
||||
let _ = self
|
||||
},
|
||||
sendGif: { _, _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
sendBotContextResultAsGif: { _, _ , _, _, _, _ in
|
||||
return false
|
||||
},
|
||||
updateChoosingSticker: { _ in
|
||||
},
|
||||
switchToTextInput: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentInputMode = .keyboard
|
||||
self.state?.updated(transition: .immediate)
|
||||
},
|
||||
dismissTextInput: {
|
||||
},
|
||||
insertText: { [weak self] text in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
for (textInputView, externalState) in self.collectTextInputStates() {
|
||||
if externalState.isEditing {
|
||||
textInputView.insertText(text: text)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
backwardsDeleteText: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
for (textInputView, externalState) in self.collectTextInputStates() {
|
||||
if externalState.isEditing {
|
||||
textInputView.backwardsDeleteText()
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
openStickerEditor: {
|
||||
},
|
||||
presentController: { [weak self] c, a in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.present(c, in: .window(.root), with: a)
|
||||
},
|
||||
presentGlobalOverlayController: { [weak self] c, a in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.environment?.controller()?.presentInGlobalOverlay(c, with: a)
|
||||
},
|
||||
getNavigationController: { [weak self] in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
return self.environment?.controller()?.navigationController as? NavigationController
|
||||
},
|
||||
requestLayout: { [weak self] transition in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: Transition(transition))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
self.component = component
|
||||
@ -244,95 +557,6 @@ final class ComposePollScreenComponent: Component {
|
||||
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
|
||||
let sectionSpacing: CGFloat = 24.0
|
||||
|
||||
if self.emojiContentDisposable == nil {
|
||||
let emojiContent = EmojiPagerContentComponent.emojiInputData(
|
||||
context: component.context,
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
isStandalone: false,
|
||||
subject: .emoji,
|
||||
hasTrending: false,
|
||||
topReactionItems: [],
|
||||
areUnicodeEmojiEnabled: false,
|
||||
areCustomEmojiEnabled: true,
|
||||
chatPeerId: nil,
|
||||
selectedItems: Set(),
|
||||
backgroundIconColor: nil,
|
||||
hasSearch: false,
|
||||
forceHasPremium: true
|
||||
)
|
||||
self.emojiContentDisposable = (emojiContent
|
||||
|> deliverOnMainQueue).start(next: { [weak self] emojiContent in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.emojiContent = emojiContent
|
||||
|
||||
emojiContent.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
|
||||
performItemAction: { [weak self] _, item, _, _, _, _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
guard let itemFile = item.itemFile else {
|
||||
return
|
||||
}
|
||||
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
|
||||
let _ = itemFile
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.25))
|
||||
}
|
||||
},
|
||||
deleteBackwards: {
|
||||
},
|
||||
openStickerSettings: {
|
||||
},
|
||||
openFeatured: {
|
||||
},
|
||||
openSearch: {
|
||||
},
|
||||
addGroupAction: { _, _, _ in
|
||||
},
|
||||
clearGroup: { _ in
|
||||
},
|
||||
editAction: { _ in
|
||||
},
|
||||
pushController: { c in
|
||||
},
|
||||
presentController: { c in
|
||||
},
|
||||
presentGlobalOverlayController: { c in
|
||||
},
|
||||
navigationController: {
|
||||
return nil
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _ in
|
||||
},
|
||||
updateScrollingToItemGroup: {
|
||||
},
|
||||
onScroll: {},
|
||||
chatPeerId: nil,
|
||||
peekBehavior: nil,
|
||||
customLayout: nil,
|
||||
externalBackground: nil,
|
||||
externalExpansionView: nil,
|
||||
customContentView: nil,
|
||||
useOpaqueTheme: true,
|
||||
hideBackground: false,
|
||||
stateContext: nil,
|
||||
addImage: nil
|
||||
)
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = environment.theme.list.blocksBackgroundColor
|
||||
}
|
||||
@ -344,23 +568,42 @@ final class ComposePollScreenComponent: Component {
|
||||
contentHeight += topInset
|
||||
|
||||
var pollTextSectionItems: [AnyComponentWithIdentity<Empty>] = []
|
||||
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
|
||||
pollTextSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListComposePollOptionComponent(
|
||||
externalState: self.pollTextInputState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
initialText: "",
|
||||
resetText: self.resetPollText.flatMap { resetPollText in
|
||||
return ListMultilineTextFieldItemComponent.ResetText(value: resetPollText)
|
||||
resetText: self.resetPollText.flatMap { resetText in
|
||||
return ListComposePollOptionComponent.ResetText(value: resetText)
|
||||
},
|
||||
placeholder: "Enter Question",
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
characterLimit: 256,
|
||||
emptyLineHandling: .oneConsecutive,
|
||||
updated: { _ in
|
||||
returnKeyAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.pollOptions.isEmpty {
|
||||
if let pollOptionView = self.pollOptionsSectionContainer.itemViews[self.pollOptions[0].id] {
|
||||
if let pollOptionComponentView = pollOptionView.contents.view as? ListComposePollOptionComponent.View {
|
||||
pollOptionComponentView.activateInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backspaceKeyAction: nil,
|
||||
selection: nil,
|
||||
inputMode: self.currentInputMode,
|
||||
toggleInputMode: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch self.currentInputMode {
|
||||
case .keyboard:
|
||||
self.currentInputMode = .emoji
|
||||
case .emoji:
|
||||
self.currentInputMode = .keyboard
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
textUpdateTransition: .spring(duration: 0.4),
|
||||
tag: self.pollTextFieldTag
|
||||
))))
|
||||
self.resetPollText = nil
|
||||
@ -385,12 +628,16 @@ final class ComposePollScreenComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let pollTextSectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: pollTextSectionSize)
|
||||
if let pollTextSectionView = self.pollTextSection.view {
|
||||
if let pollTextSectionView = self.pollTextSection.view as? ListSectionComponent.View {
|
||||
if pollTextSectionView.superview == nil {
|
||||
self.scrollView.addSubview(pollTextSectionView)
|
||||
self.pollTextSection.parentState = state
|
||||
}
|
||||
transition.setFrame(view: pollTextSectionView, frame: pollTextSectionFrame)
|
||||
|
||||
if let itemView = pollTextSectionView.itemView(id: 0) as? ListComposePollOptionComponent.View {
|
||||
itemView.updateCustomPlaceholder(value: "Ask a Question", size: itemView.bounds.size, transition: .immediate)
|
||||
}
|
||||
}
|
||||
contentHeight += pollTextSectionSize.height
|
||||
contentHeight += sectionSpacing
|
||||
@ -454,7 +701,21 @@ final class ComposePollScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
},
|
||||
selection: optionSelection
|
||||
selection: optionSelection,
|
||||
inputMode: self.currentInputMode,
|
||||
toggleInputMode: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
switch self.currentInputMode {
|
||||
case .keyboard:
|
||||
self.currentInputMode = .emoji
|
||||
case .emoji:
|
||||
self.currentInputMode = .keyboard
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
tag: pollOption.textFieldTag
|
||||
))))
|
||||
|
||||
let item = pollOptionsSectionItems[i]
|
||||
@ -475,7 +736,7 @@ final class ComposePollScreenComponent: Component {
|
||||
transition: itemTransition,
|
||||
component: item.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
pollOptionsSectionReadyItems.append(ListSectionContentView.ReadyItem(
|
||||
@ -530,6 +791,7 @@ final class ComposePollScreenComponent: Component {
|
||||
background: .all
|
||||
),
|
||||
width: availableSize.width - sideInset * 2.0,
|
||||
leftInset: 0.0,
|
||||
readyItems: pollOptionsSectionReadyItems,
|
||||
transition: transition
|
||||
)
|
||||
@ -777,65 +1039,156 @@ final class ComposePollScreenComponent: Component {
|
||||
}
|
||||
|
||||
var inputHeight: CGFloat = 0.0
|
||||
if self.displayInput, let emojiContent = self.emojiContent {
|
||||
let reactionSelectionControl: ComponentView<Empty>
|
||||
var animateIn = false
|
||||
if let current = self.reactionSelectionControl {
|
||||
reactionSelectionControl = current
|
||||
} else {
|
||||
animateIn = true
|
||||
reactionSelectionControl = ComponentView()
|
||||
self.reactionSelectionControl = reactionSelectionControl
|
||||
inputHeight += self.updateInputMediaNode(
|
||||
component: component,
|
||||
availableSize: availableSize,
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
inputHeight: 0.0,
|
||||
effectiveInputHeight: environment.deviceMetrics.standardInputHeight(inLandscape: false),
|
||||
metrics: environment.metrics,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
transition: transition
|
||||
)
|
||||
|
||||
let textInputStates = self.collectTextInputStates()
|
||||
|
||||
let isEditing = textInputStates.contains(where: { $0.state.isEditing })
|
||||
|
||||
if let (_, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, emojiSuggestion.disposable == nil {
|
||||
emojiSuggestion.disposable = (EmojiSuggestionsComponent.suggestionData(context: component.context, isSavedMessages: false, query: emojiSuggestion.position.value)
|
||||
|> deliverOnMainQueue).start(next: { [weak self, weak suggestionTextInputState, weak emojiSuggestion] result in
|
||||
guard let self, let suggestionTextInputState, let emojiSuggestion, suggestionTextInputState.currentEmojiSuggestion === emojiSuggestion else {
|
||||
return
|
||||
}
|
||||
|
||||
emojiSuggestion.value = result
|
||||
self.state?.updated()
|
||||
})
|
||||
}
|
||||
|
||||
for (_, suggestionTextInputState) in textInputStates {
|
||||
var hasTrackingView = suggestionTextInputState.hasTrackingView
|
||||
if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = currentEmojiSuggestion.value as? [TelegramMediaFile], value.isEmpty {
|
||||
hasTrackingView = false
|
||||
}
|
||||
let reactionSelectionControlSize = reactionSelectionControl.update(
|
||||
transition: animateIn ? .immediate : transition,
|
||||
component: AnyComponent(EmojiSelectionComponent(
|
||||
theme: environment.theme,
|
||||
strings: environment.strings,
|
||||
sideInset: environment.safeInsets.left,
|
||||
bottomInset: environment.safeInsets.bottom,
|
||||
deviceMetrics: environment.deviceMetrics,
|
||||
emojiContent: emojiContent,
|
||||
stickerContent: nil,
|
||||
backgroundIconColor: nil,
|
||||
backgroundColor: environment.theme.list.itemBlocksBackgroundColor,
|
||||
separatorColor: environment.theme.list.itemBlocksSeparatorColor,
|
||||
backspace: { [weak self] in
|
||||
guard let self else {
|
||||
if !suggestionTextInputState.isEditing {
|
||||
hasTrackingView = false
|
||||
}
|
||||
|
||||
if !hasTrackingView {
|
||||
if let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion {
|
||||
suggestionTextInputState.currentEmojiSuggestion = nil
|
||||
currentEmojiSuggestion.disposable?.dispose()
|
||||
}
|
||||
|
||||
if let currentEmojiSuggestionView = self.currentEmojiSuggestionView {
|
||||
self.currentEmojiSuggestionView = nil
|
||||
|
||||
currentEmojiSuggestionView.alpha = 0.0
|
||||
currentEmojiSuggestionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak currentEmojiSuggestionView] _ in
|
||||
currentEmojiSuggestionView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let (suggestionTextInputView, suggestionTextInputState) = textInputStates.first(where: { $0.state.isEditing && $0.state.currentEmojiSuggestion != nil }), let emojiSuggestion = suggestionTextInputState.currentEmojiSuggestion, let value = emojiSuggestion.value as? [TelegramMediaFile] {
|
||||
let currentEmojiSuggestionView: ComponentHostView<Empty>
|
||||
if let current = self.currentEmojiSuggestionView {
|
||||
currentEmojiSuggestionView = current
|
||||
} else {
|
||||
currentEmojiSuggestionView = ComponentHostView<Empty>()
|
||||
self.currentEmojiSuggestionView = currentEmojiSuggestionView
|
||||
self.addSubview(currentEmojiSuggestionView)
|
||||
|
||||
currentEmojiSuggestionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
|
||||
let globalPosition: CGPoint
|
||||
if let textView = suggestionTextInputView.textFieldView {
|
||||
globalPosition = textView.convert(emojiSuggestion.localPosition, to: self)
|
||||
} else {
|
||||
globalPosition = .zero
|
||||
}
|
||||
|
||||
let sideInset: CGFloat = 7.0
|
||||
|
||||
let viewSize = currentEmojiSuggestionView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiSuggestionsComponent(
|
||||
context: component.context,
|
||||
userLocation: .other,
|
||||
theme: EmojiSuggestionsComponent.Theme(theme: environment.theme),
|
||||
animationCache: component.context.animationCache,
|
||||
animationRenderer: component.context.animationRenderer,
|
||||
files: value,
|
||||
action: { [weak self, weak suggestionTextInputView, weak suggestionTextInputState] file in
|
||||
guard let self, let suggestionTextInputView, let suggestionTextInputState, let textView = suggestionTextInputView.textFieldView, let currentEmojiSuggestion = suggestionTextInputState.currentEmojiSuggestion else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.isUpdating {
|
||||
self.state?.updated(transition: .spring(duration: 0.25))
|
||||
let _ = self
|
||||
|
||||
AudioServicesPlaySystemSound(0x450)
|
||||
|
||||
let inputState = textView.getInputState()
|
||||
let inputText = NSMutableAttributedString(attributedString: inputState.inputText)
|
||||
|
||||
var text: String?
|
||||
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
||||
loop: for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, _, displayText, _):
|
||||
text = displayText
|
||||
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: file.fileId.id, file: file)
|
||||
break loop
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if let emojiAttribute = emojiAttribute, let text = text {
|
||||
let replacementText = NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute])
|
||||
|
||||
let range = currentEmojiSuggestion.position.range
|
||||
let previousText = inputText.attributedSubstring(from: range)
|
||||
inputText.replaceCharacters(in: range, with: replacementText)
|
||||
|
||||
var replacedUpperBound = range.lowerBound
|
||||
while true {
|
||||
if inputText.attributedSubstring(from: NSRange(location: 0, length: replacedUpperBound)).string.hasSuffix(previousText.string) {
|
||||
let replaceRange = NSRange(location: replacedUpperBound - previousText.length, length: previousText.length)
|
||||
if replaceRange.location < 0 {
|
||||
break
|
||||
}
|
||||
let adjacentString = inputText.attributedSubstring(from: replaceRange)
|
||||
if adjacentString.string != previousText.string || adjacentString.attribute(ChatTextInputAttributes.customEmoji, at: 0, effectiveRange: nil) != nil {
|
||||
break
|
||||
}
|
||||
inputText.replaceCharacters(in: replaceRange, with: NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: emojiAttribute.interactivelySelectedFromPackId, fileId: emojiAttribute.fileId, file: emojiAttribute.file)]))
|
||||
replacedUpperBound = replaceRange.lowerBound
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
let selectionPosition = range.lowerBound + (replacementText.string as NSString).length
|
||||
textView.updateText(inputText, selectionRange: selectionPosition ..< selectionPosition)
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 100.0)
|
||||
)
|
||||
let reactionSelectionControlFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - reactionSelectionControlSize.height), size: reactionSelectionControlSize)
|
||||
if let reactionSelectionControlView = reactionSelectionControl.view {
|
||||
if reactionSelectionControlView.superview == nil {
|
||||
self.addSubview(reactionSelectionControlView)
|
||||
}
|
||||
if animateIn {
|
||||
reactionSelectionControlView.frame = reactionSelectionControlFrame
|
||||
transition.animatePosition(view: reactionSelectionControlView, from: CGPoint(x: 0.0, y: reactionSelectionControlFrame.height), to: CGPoint(), additive: true)
|
||||
} else {
|
||||
transition.setFrame(view: reactionSelectionControlView, frame: reactionSelectionControlFrame)
|
||||
}
|
||||
}
|
||||
inputHeight = reactionSelectionControlSize.height
|
||||
} else if let reactionSelectionControl = self.reactionSelectionControl {
|
||||
self.reactionSelectionControl = nil
|
||||
if let reactionSelectionControlView = reactionSelectionControl.view {
|
||||
transition.setPosition(view: reactionSelectionControlView, position: CGPoint(x: reactionSelectionControlView.center.x, y: availableSize.height + reactionSelectionControlView.bounds.height * 0.5), completion: { [weak reactionSelectionControlView] _ in
|
||||
reactionSelectionControlView?.removeFromSuperview()
|
||||
})
|
||||
|
||||
let viewFrame = CGRect(origin: CGPoint(x: min(availableSize.width - sideInset - viewSize.width, max(sideInset, floor(globalPosition.x - viewSize.width / 2.0))), y: globalPosition.y - 4.0 - viewSize.height), size: viewSize)
|
||||
currentEmojiSuggestionView.frame = viewFrame
|
||||
if let componentView = currentEmojiSuggestionView.componentView as? EmojiSuggestionsComponent.View {
|
||||
componentView.adjustBackground(relativePositionX: floor(globalPosition.x + 10.0))
|
||||
}
|
||||
}
|
||||
|
||||
if self.displayInput {
|
||||
if isEditing {
|
||||
contentHeight += bottomInset + 8.0
|
||||
contentHeight += inputHeight
|
||||
} else {
|
||||
@ -857,7 +1210,7 @@ final class ComposePollScreenComponent: Component {
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
if self.pollTextInputState.isEditing || self.pollOptions.contains(where: { $0.textInputState.isEditing }) {
|
||||
if isEditing {
|
||||
if let controller = environment.controller() as? ComposePollScreen {
|
||||
DispatchQueue.main.async { [weak controller] in
|
||||
controller?.requestAttachmentMenuExpansion()
|
||||
@ -908,6 +1261,15 @@ public class ComposePollScreen: ViewControllerComponentContainer, AttachmentCont
|
||||
}
|
||||
public var mediaPickerContext: AttachmentMediaPickerContext?
|
||||
|
||||
public var isPanGestureEnabled: (() -> Bool)? {
|
||||
return { [weak self] in
|
||||
guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
|
||||
return true
|
||||
}
|
||||
return componentView.isPanGestureEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
peer: EnginePeer,
|
||||
|
@ -488,10 +488,20 @@ private func createPollControllerEntries(presentationData: PresentationData, pee
|
||||
}
|
||||
|
||||
public final class ComposedPoll {
|
||||
public struct Text {
|
||||
public let string: String
|
||||
public let entities: [MessageTextEntity]
|
||||
|
||||
public init(string: String, entities: [MessageTextEntity]) {
|
||||
self.string = string
|
||||
self.entities = entities
|
||||
}
|
||||
}
|
||||
|
||||
public let publicity: TelegramMediaPollPublicity
|
||||
public let kind: TelegramMediaPollKind
|
||||
|
||||
public let text: String
|
||||
public let text: Text
|
||||
public let options: [TelegramMediaPollOption]
|
||||
public let correctAnswers: [Data]?
|
||||
public let results: TelegramMediaPollResults
|
||||
@ -500,7 +510,7 @@ public final class ComposedPoll {
|
||||
public init(
|
||||
publicity: TelegramMediaPollPublicity,
|
||||
kind: TelegramMediaPollKind,
|
||||
text: String,
|
||||
text: Text,
|
||||
options: [TelegramMediaPollOption],
|
||||
correctAnswers: [Data]?,
|
||||
results: TelegramMediaPollResults,
|
||||
@ -928,7 +938,7 @@ public func createPollController(context: AccountContext, updatedPresentationDat
|
||||
let optionText = state.options[i].item.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if !optionText.isEmpty {
|
||||
let optionData = "\(i)".data(using: .utf8)!
|
||||
options.append(TelegramMediaPollOption(text: optionText, opaqueIdentifier: optionData))
|
||||
options.append(TelegramMediaPollOption(text: optionText, entities: [], opaqueIdentifier: optionData))
|
||||
if state.isQuiz && state.options[i].item.isSelected {
|
||||
correctAnswers = [optionData]
|
||||
}
|
||||
@ -959,7 +969,7 @@ public func createPollController(context: AccountContext, updatedPresentationDat
|
||||
completion(ComposedPoll(
|
||||
publicity: publicity,
|
||||
kind: kind,
|
||||
text: processPollText(state.text),
|
||||
text: ComposedPoll.Text(string: processPollText(state.text), entities: []),
|
||||
options: options,
|
||||
correctAnswers: correctAnswers,
|
||||
results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: resolvedSolution),
|
||||
|
@ -10,17 +10,11 @@ import TextFieldComponent
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import PresentationDataUtils
|
||||
import LottieComponent
|
||||
import PlainButtonComponent
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class ListComposePollOptionComponent: Component {
|
||||
public final class ExternalState {
|
||||
public fileprivate(set) var hasText: Bool = false
|
||||
public fileprivate(set) var text: NSAttributedString = NSAttributedString()
|
||||
public fileprivate(set) var isEditing: Bool = false
|
||||
|
||||
public init() {
|
||||
}
|
||||
}
|
||||
|
||||
public final class ResetText: Equatable {
|
||||
public let value: String
|
||||
|
||||
@ -50,7 +44,31 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public let externalState: ExternalState?
|
||||
public enum InputMode {
|
||||
case keyboard
|
||||
case emoji
|
||||
}
|
||||
|
||||
public final class EmojiSuggestion {
|
||||
public struct Position: Equatable {
|
||||
public var range: NSRange
|
||||
public var value: String
|
||||
}
|
||||
|
||||
public var localPosition: CGPoint
|
||||
public var position: Position
|
||||
public var disposable: Disposable?
|
||||
public var value: Any?
|
||||
|
||||
init(localPosition: CGPoint, position: Position) {
|
||||
self.localPosition = localPosition
|
||||
self.position = position
|
||||
self.disposable = nil
|
||||
self.value = nil
|
||||
}
|
||||
}
|
||||
|
||||
public let externalState: TextFieldComponent.ExternalState?
|
||||
public let context: AccountContext
|
||||
public let theme: PresentationTheme
|
||||
public let strings: PresentationStrings
|
||||
@ -59,9 +77,12 @@ public final class ListComposePollOptionComponent: Component {
|
||||
public let returnKeyAction: (() -> Void)?
|
||||
public let backspaceKeyAction: (() -> Void)?
|
||||
public let selection: Selection?
|
||||
public let inputMode: InputMode?
|
||||
public let toggleInputMode: (() -> Void)?
|
||||
public let tag: AnyObject?
|
||||
|
||||
public init(
|
||||
externalState: ExternalState?,
|
||||
externalState: TextFieldComponent.ExternalState?,
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
@ -69,7 +90,10 @@ public final class ListComposePollOptionComponent: Component {
|
||||
characterLimit: Int,
|
||||
returnKeyAction: (() -> Void)?,
|
||||
backspaceKeyAction: (() -> Void)?,
|
||||
selection: Selection?
|
||||
selection: Selection?,
|
||||
inputMode: InputMode?,
|
||||
toggleInputMode: (() -> Void)?,
|
||||
tag: AnyObject? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
self.context = context
|
||||
@ -80,6 +104,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.returnKeyAction = returnKeyAction
|
||||
self.backspaceKeyAction = backspaceKeyAction
|
||||
self.selection = selection
|
||||
self.inputMode = inputMode
|
||||
self.toggleInputMode = toggleInputMode
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
public static func ==(lhs: ListComposePollOptionComponent, rhs: ListComposePollOptionComponent) -> Bool {
|
||||
@ -104,6 +131,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
if lhs.selection != rhs.selection {
|
||||
return false
|
||||
}
|
||||
if lhs.inputMode != rhs.inputMode {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -181,9 +211,10 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, ListSectionComponent.ChildView {
|
||||
public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView {
|
||||
private let textField = ComponentView<Empty>()
|
||||
private let textFieldExternalState = TextFieldComponent.ExternalState()
|
||||
|
||||
private var modeSelector: ComponentView<Empty>?
|
||||
|
||||
private var checkView: CheckView?
|
||||
|
||||
@ -201,6 +232,10 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public var textFieldView: TextFieldComponent.View? {
|
||||
return self.textField.view as? TextFieldComponent.View
|
||||
}
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
@ -212,40 +247,68 @@ public final class ListComposePollOptionComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
if componentTag === tag {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public func activateInput() {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.activateInput()
|
||||
}
|
||||
}
|
||||
|
||||
public func insertText(text: NSAttributedString) {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.insertText(text)
|
||||
}
|
||||
}
|
||||
|
||||
public func backwardsDeleteText() {
|
||||
if let textFieldView = self.textField.view as? TextFieldComponent.View {
|
||||
textFieldView.deleteBackward()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
self.isUpdating = false
|
||||
}
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let verticalInset: CGFloat = 12.0
|
||||
var leftInset: CGFloat = 16.0
|
||||
let rightInset: CGFloat = 16.0
|
||||
var rightInset: CGFloat = 16.0
|
||||
let modeSelectorSize = CGSize(width: 32.0, height: 32.0)
|
||||
|
||||
if component.selection != nil {
|
||||
leftInset += 34.0
|
||||
}
|
||||
|
||||
if component.inputMode != nil {
|
||||
rightInset += 34.0
|
||||
}
|
||||
|
||||
let textFieldSize = self.textField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TextFieldComponent(
|
||||
context: component.context,
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
externalState: self.textFieldExternalState,
|
||||
externalState: component.externalState ?? TextFieldComponent.ExternalState(),
|
||||
fontSize: 17.0,
|
||||
textColor: component.theme.list.itemPrimaryTextColor,
|
||||
insets: UIEdgeInsets(top: verticalInset, left: 8.0, bottom: verticalInset, right: 8.0),
|
||||
hideKeyboard: false,
|
||||
hideKeyboard: component.inputMode == .emoji,
|
||||
customInputView: nil,
|
||||
resetText: component.resetText.flatMap { resetText in
|
||||
return NSAttributedString(string: resetText.value, font: Font.regular(17.0), textColor: component.theme.list.itemPrimaryTextColor)
|
||||
@ -275,10 +338,10 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: availableSize.height)
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset + 8.0 * 2.0, height: availableSize.height)
|
||||
)
|
||||
|
||||
let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0)
|
||||
let size = CGSize(width: availableSize.width, height: textFieldSize.height - 1.0)
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0, y: 0.0), size: textFieldSize)
|
||||
|
||||
if let textFieldView = self.textField.view {
|
||||
@ -327,11 +390,73 @@ public final class ListComposePollOptionComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
self.separatorInset = leftInset
|
||||
if let inputMode = component.inputMode {
|
||||
var modeSelectorTransition = transition
|
||||
let modeSelector: ComponentView<Empty>
|
||||
if let current = self.modeSelector {
|
||||
modeSelector = current
|
||||
} else {
|
||||
modeSelectorTransition = modeSelectorTransition.withAnimation(.none)
|
||||
modeSelector = ComponentView()
|
||||
self.modeSelector = modeSelector
|
||||
}
|
||||
let animationName: String
|
||||
var playAnimation = false
|
||||
if let previousComponent, let previousInputMode = previousComponent.inputMode {
|
||||
if previousInputMode != inputMode {
|
||||
playAnimation = true
|
||||
}
|
||||
}
|
||||
switch inputMode {
|
||||
case .keyboard:
|
||||
animationName = "input_anim_keyToSmile"
|
||||
case .emoji:
|
||||
animationName = "input_anim_smileToKey"
|
||||
}
|
||||
|
||||
let _ = modeSelector.update(
|
||||
transition: modeSelectorTransition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(LottieComponent(
|
||||
content: LottieComponent.AppBundleContent(
|
||||
name: animationName
|
||||
),
|
||||
color: component.theme.chat.inputPanel.inputControlColor.blitOver(component.theme.list.itemBlocksBackgroundColor, alpha: 1.0),
|
||||
size: modeSelectorSize
|
||||
)),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.toggleInputMode?()
|
||||
},
|
||||
animateScale: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: modeSelectorSize
|
||||
)
|
||||
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||
if modeSelectorView.superview == nil {
|
||||
self.addSubview(modeSelectorView)
|
||||
}
|
||||
|
||||
if playAnimation, let animationView = modeSelectorView.contentView as? LottieComponent.View {
|
||||
animationView.playOnce()
|
||||
}
|
||||
|
||||
modeSelectorTransition.setFrame(view: modeSelectorView, frame: modeSelectorFrame)
|
||||
if let externalState = component.externalState {
|
||||
modeSelectorView.isHidden = !externalState.isEditing
|
||||
}
|
||||
}
|
||||
} else if let modeSelector = self.modeSelector {
|
||||
self.modeSelector = nil
|
||||
modeSelector.view?.removeFromSuperview()
|
||||
}
|
||||
|
||||
component.externalState?.hasText = self.textFieldExternalState.hasText
|
||||
component.externalState?.text = self.textFieldExternalState.text
|
||||
component.externalState?.isEditing = self.textFieldExternalState.isEditing
|
||||
self.separatorInset = leftInset
|
||||
|
||||
return size
|
||||
}
|
||||
@ -378,7 +503,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
transition.setPosition(view: placeholderView, position: placeholderFrame.origin)
|
||||
placeholderView.bounds = CGRect(origin: CGPoint(), size: placeholderFrame.size)
|
||||
|
||||
placeholderView.isHidden = self.textFieldExternalState.hasText
|
||||
if let externalState = component.externalState {
|
||||
placeholderView.isHidden = externalState.hasText
|
||||
}
|
||||
}
|
||||
} else if let customPlaceholder = self.customPlaceholder {
|
||||
self.customPlaceholder = nil
|
||||
|
@ -41,49 +41,47 @@ static const void *positionChangedKey = &positionChangedKey;
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self != nil) {
|
||||
if (iosMajorVersion() >= 8) {
|
||||
_offIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOff.png")];
|
||||
_onIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOn.png")];
|
||||
self.layer.cornerRadius = 17.0f;
|
||||
self.backgroundColor = [UIColor redColor];
|
||||
self.tintColor = [UIColor redColor];
|
||||
UIView *handleView = self.subviews[0].subviews.lastObject;
|
||||
if (iosMajorVersion() >= 13) {
|
||||
handleView = self.subviews[0].subviews[1].subviews.lastObject;
|
||||
} else {
|
||||
handleView = self.subviews[0].subviews.lastObject;
|
||||
}
|
||||
|
||||
static Class subclass;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
subclass = freedomMakeClass([handleView.layer class], [TGBaseIconSwitch class]);
|
||||
object_setClass(handleView.layer, subclass);
|
||||
});
|
||||
|
||||
CGPoint offset = CGPointZero;
|
||||
if (iosMajorVersion() >= 12) {
|
||||
offset = CGPointMake(-7.0, -3.0);
|
||||
}
|
||||
|
||||
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y);
|
||||
_onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y);
|
||||
[handleView addSubview:_onIconView];
|
||||
[handleView addSubview:_offIconView];
|
||||
|
||||
_onIconView.alpha = 0.0f;
|
||||
|
||||
[self addTarget:self action:@selector(currentValueChanged) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
__weak TGIconSwitchView *weakSelf = self;
|
||||
void (^block)(CGPoint) = ^(CGPoint point) {
|
||||
__strong TGIconSwitchView *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf updateState:point.x > 30.0 animated:true force:false];
|
||||
}
|
||||
};
|
||||
objc_setAssociatedObject(handleView.layer, positionChangedKey, [block copy], OBJC_ASSOCIATION_RETAIN);
|
||||
_offIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOff.png")];
|
||||
_onIconView = [[UIImageView alloc] initWithImage:TGComponentsImageNamed(@"PermissionSwitchOn.png")];
|
||||
self.layer.cornerRadius = 17.0f;
|
||||
self.backgroundColor = [UIColor redColor];
|
||||
self.tintColor = [UIColor redColor];
|
||||
UIView *handleView = self.subviews[0].subviews.lastObject;
|
||||
if (iosMajorVersion() >= 13) {
|
||||
handleView = self.subviews[0].subviews[1].subviews.lastObject;
|
||||
} else {
|
||||
handleView = self.subviews[0].subviews.lastObject;
|
||||
}
|
||||
|
||||
static Class subclass;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
subclass = freedomMakeClass([handleView.layer class], [TGBaseIconSwitch class]);
|
||||
object_setClass(handleView.layer, subclass);
|
||||
});
|
||||
|
||||
CGPoint offset = CGPointZero;
|
||||
if (iosMajorVersion() >= 12) {
|
||||
offset = CGPointMake(-7.0, -3.0);
|
||||
}
|
||||
|
||||
_offIconView.frame = CGRectOffset(_offIconView.bounds, TGScreenPixelFloor(21.5f) + offset.x, TGScreenPixelFloor(14.5f) + offset.y);
|
||||
_onIconView.frame = CGRectOffset(_onIconView.bounds, 20.0f + offset.x, 15.0f + offset.y);
|
||||
[handleView addSubview:_onIconView];
|
||||
[handleView addSubview:_offIconView];
|
||||
|
||||
_onIconView.alpha = 0.0f;
|
||||
|
||||
[self addTarget:self action:@selector(currentValueChanged) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
__weak TGIconSwitchView *weakSelf = self;
|
||||
void (^block)(CGPoint) = ^(CGPoint point) {
|
||||
__strong TGIconSwitchView *strongSelf = weakSelf;
|
||||
if (strongSelf != nil) {
|
||||
[strongSelf updateState:point.x > 30.0 animated:true force:false];
|
||||
}
|
||||
};
|
||||
objc_setAssociatedObject(handleView.layer, positionChangedKey, [block copy], OBJC_ASSOCIATION_RETAIN);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
@ -2012,11 +2012,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
backgroundColor: gradientColors[i],
|
||||
foregroundColor: .white,
|
||||
iconName: perk.iconName
|
||||
))),
|
||||
)))),
|
||||
action: { [weak state] _ in
|
||||
var demoSubject: PremiumDemoScreen.Subject
|
||||
switch perk {
|
||||
@ -2179,11 +2179,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
backgroundColor: gradientColors[min(i, gradientColors.count - 1)],
|
||||
foregroundColor: .white,
|
||||
iconName: perk.iconName
|
||||
))),
|
||||
)))),
|
||||
action: { [weak state] _ in
|
||||
let isPremium = state?.isPremium == true
|
||||
if isPremium {
|
||||
@ -2363,11 +2363,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
backgroundColor: UIColor(rgb: 0x676bff),
|
||||
foregroundColor: .white,
|
||||
iconName: "Premium/BusinessPerk/Status"
|
||||
))),
|
||||
)))),
|
||||
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(EmojiActionIconComponent(
|
||||
context: context.component.context,
|
||||
color: accentColor,
|
||||
@ -2404,11 +2404,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
backgroundColor: UIColor(rgb: 0x4492ff),
|
||||
foregroundColor: .white,
|
||||
iconName: "Premium/BusinessPerk/Tag"
|
||||
))),
|
||||
)))),
|
||||
action: { _ in
|
||||
push(accountContext.sharedContext.makeFilterSettingsController(context: accountContext, modal: false, scrollToTags: true, dismissed: nil))
|
||||
}
|
||||
@ -2435,11 +2435,11 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
||||
lineSpacing: 0.18
|
||||
)))
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(PerkIconComponent(
|
||||
backgroundColor: UIColor(rgb: 0x41a6a5),
|
||||
foregroundColor: .white,
|
||||
iconName: "Premium/Perk/Stories"
|
||||
))),
|
||||
)))),
|
||||
action: { _ in
|
||||
push(accountContext.sharedContext.makeMyStoriesController(context: accountContext, isArchive: false))
|
||||
}
|
||||
|
@ -433,7 +433,7 @@ func textMediaAndExpirationTimerFromApiMedia(_ media: Api.MessageMedia?, _ peerI
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil)
|
||||
return (TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, textEntities: [], options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod), nil, nil, nil, nil)
|
||||
}
|
||||
case let .messageMediaDice(value, emoticon):
|
||||
return (TelegramMediaDice(emoji: emoticon, value: value), nil, nil, nil, nil)
|
||||
|
@ -7,7 +7,7 @@ extension TelegramMediaPollOption {
|
||||
init(apiOption: Api.PollAnswer) {
|
||||
switch apiOption {
|
||||
case let .pollAnswer(text, option):
|
||||
self.init(text: text, opaqueIdentifier: option.makeData())
|
||||
self.init(text: text, entities: [], opaqueIdentifier: option.makeData())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3897,7 +3897,7 @@ func replayFinalState(
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
updatedPoll = TelegramMediaPoll(pollId: MediaId(namespace: Namespaces.Media.CloudPoll, id: id), publicity: publicity, kind: kind, text: question, textEntities: [], options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: poll.results, isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
}
|
||||
}
|
||||
updatedPoll = updatedPoll.withUpdatedResults(TelegramMediaPollResults(apiResults: results), min: resultsMin)
|
||||
|
@ -3,20 +3,24 @@ import Postbox
|
||||
|
||||
public struct TelegramMediaPollOption: Equatable, PostboxCoding {
|
||||
public let text: String
|
||||
public let entities: [MessageTextEntity]
|
||||
public let opaqueIdentifier: Data
|
||||
|
||||
public init(text: String, opaqueIdentifier: Data) {
|
||||
public init(text: String, entities: [MessageTextEntity], opaqueIdentifier: Data) {
|
||||
self.text = text
|
||||
self.entities = entities
|
||||
self.opaqueIdentifier = opaqueIdentifier
|
||||
}
|
||||
|
||||
public init(decoder: PostboxDecoder) {
|
||||
self.text = decoder.decodeStringForKey("t", orElse: "")
|
||||
self.entities = decoder.decodeObjectArrayWithDecoderForKey("et")
|
||||
self.opaqueIdentifier = decoder.decodeDataForKey("i") ?? Data()
|
||||
}
|
||||
|
||||
public func encode(_ encoder: PostboxEncoder) {
|
||||
encoder.encodeString(self.text, forKey: "t")
|
||||
encoder.encodeObjectArray(self.entities, forKey: "et")
|
||||
encoder.encodeData(self.opaqueIdentifier, forKey: "i")
|
||||
}
|
||||
}
|
||||
@ -150,17 +154,19 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
public let kind: TelegramMediaPollKind
|
||||
|
||||
public let text: String
|
||||
public let textEntities: [MessageTextEntity]
|
||||
public let options: [TelegramMediaPollOption]
|
||||
public let correctAnswers: [Data]?
|
||||
public let results: TelegramMediaPollResults
|
||||
public let isClosed: Bool
|
||||
public let deadlineTimeout: Int32?
|
||||
|
||||
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) {
|
||||
public init(pollId: MediaId, publicity: TelegramMediaPollPublicity, kind: TelegramMediaPollKind, text: String, textEntities: [MessageTextEntity], options: [TelegramMediaPollOption], correctAnswers: [Data]?, results: TelegramMediaPollResults, isClosed: Bool, deadlineTimeout: Int32?) {
|
||||
self.pollId = pollId
|
||||
self.publicity = publicity
|
||||
self.kind = kind
|
||||
self.text = text
|
||||
self.textEntities = textEntities
|
||||
self.options = options
|
||||
self.correctAnswers = correctAnswers
|
||||
self.results = results
|
||||
@ -177,6 +183,7 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
self.publicity = TelegramMediaPollPublicity(rawValue: decoder.decodeInt32ForKey("pb", orElse: 0)) ?? TelegramMediaPollPublicity.anonymous
|
||||
self.kind = decoder.decodeObjectForKey("kn", decoder: { TelegramMediaPollKind(decoder: $0) }) as? TelegramMediaPollKind ?? TelegramMediaPollKind.poll(multipleAnswers: false)
|
||||
self.text = decoder.decodeStringForKey("t", orElse: "")
|
||||
self.textEntities = decoder.decodeObjectArrayWithDecoderForKey("te")
|
||||
self.options = decoder.decodeObjectArrayWithDecoderForKey("os")
|
||||
self.correctAnswers = decoder.decodeOptionalDataArrayForKey("ca")
|
||||
self.results = decoder.decodeObjectForKey("rs", decoder: { TelegramMediaPollResults(decoder: $0) }) as? TelegramMediaPollResults ?? TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil)
|
||||
@ -191,6 +198,7 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
encoder.encodeObject(self.kind, forKey: "kn")
|
||||
encoder.encodeBytes(buffer, forKey: "i")
|
||||
encoder.encodeString(self.text, forKey: "t")
|
||||
encoder.encodeObjectArray(self.textEntities, forKey: "te")
|
||||
encoder.encodeObjectArray(self.options, forKey: "os")
|
||||
if let correctAnswers = self.correctAnswers {
|
||||
encoder.encodeDataArray(correctAnswers, forKey: "ca")
|
||||
@ -230,6 +238,9 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.textEntities != rhs.textEntities {
|
||||
return false
|
||||
}
|
||||
if lhs.options != rhs.options {
|
||||
return false
|
||||
}
|
||||
@ -273,6 +284,6 @@ public final class TelegramMediaPoll: Media, Equatable {
|
||||
} else {
|
||||
updatedResults = results
|
||||
}
|
||||
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout)
|
||||
return TelegramMediaPoll(pollId: self.pollId, publicity: self.publicity, kind: self.kind, text: self.text, textEntities: self.textEntities, options: self.options, correctAnswers: self.correctAnswers, results: updatedResults, isClosed: self.isClosed, deadlineTimeout: self.deadlineTimeout)
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ func _internal_requestMessageSelectPollOption(account: Account, messageId: Messa
|
||||
} else {
|
||||
kind = .poll(multipleAnswers: (flags & (1 << 2)) != 0)
|
||||
}
|
||||
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
resultPoll = TelegramMediaPoll(pollId: pollId, publicity: publicity, kind: kind, text: question, textEntities: [], options: answers.map(TelegramMediaPollOption.init(apiOption:)), correctAnswers: nil, results: TelegramMediaPollResults(apiResults: results), isClosed: (flags & (1 << 0)) != 0, deadlineTimeout: closePeriod)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -446,6 +446,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Ads/AdsInfoScreen",
|
||||
"//submodules/TelegramUI/Components/Ads/AdsReportScreen",
|
||||
"//submodules/TelegramUI/Components/Settings/BotSettingsScreen",
|
||||
"//submodules/TelegramUI/Components/AdminUserActionsSheet",
|
||||
] + select({
|
||||
"@build_bazel_rules_apple//apple:ios_arm64": appcenter_targets,
|
||||
"//build-system:ios_sim_arm64": [],
|
||||
|
37
submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD
Normal file
37
submodules/TelegramUI/Components/AdminUserActionsSheet/BUILD
Normal file
@ -0,0 +1,37 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "AdminUserActionsSheet",
|
||||
module_name = "AdminUserActionsSheet",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/AsyncDisplayKit",
|
||||
"//submodules/Display",
|
||||
"//submodules/Postbox",
|
||||
"//submodules/TelegramCore",
|
||||
"//submodules/SSignalKit/SwiftSignalKit",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/ComponentDisplayAdapters",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/AccountContext",
|
||||
"//submodules/AppBundle",
|
||||
"//submodules/PresentationDataUtils",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/UndoUI",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
@ -0,0 +1,342 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import TelegramStringFormatting
|
||||
import ListSectionComponent
|
||||
|
||||
/*final class AdminUserActionsSwitchComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool)
|
||||
}
|
||||
|
||||
enum SubtitleIcon {
|
||||
case lock
|
||||
}
|
||||
|
||||
enum Subtitle: Equatable {
|
||||
case presence(EnginePeer.Presence?)
|
||||
case text(text: String, icon: SubtitleIcon)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let subtitle: Subtitle
|
||||
let peer: EnginePeer?
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
subtitle: Subtitle,
|
||||
peer: EnginePeer?,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.peer = peer
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: AdminUserActionsSwitchComponent, rhs: AdminUserActionsSwitchComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.subtitle != rhs.subtitle {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
if lhs.hasNext != rhs.hasNext {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var labelIconView: UIImageView?
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var component: AdminUserActionsSwitchComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.addSubview(self.containerButton)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer)
|
||||
}
|
||||
|
||||
func update(component: AdminUserActionsSwitchComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var hasSelectionUpdated = false
|
||||
if let previousComponent = self.component {
|
||||
switch previousComponent.selectionState {
|
||||
case .none:
|
||||
if case .none = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
case .editing:
|
||||
if case .editing = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
|
||||
let height: CGFloat = 60.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
let leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||
let avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
if case let .editing(isSelected) = component.selectionState {
|
||||
rightInset += 48.0
|
||||
|
||||
let checkSize: CGFloat = 22.0
|
||||
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
if themeUpdated {
|
||||
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
self.checkLayer = checkLayer
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
}
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset + floor((48.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||
checkLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} else {
|
||||
clipStyle = .round
|
||||
}
|
||||
if peer.id == component.context.account.peerId {
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
} else {
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
}
|
||||
|
||||
var labelIcon: UIImage?
|
||||
let labelData: (String, Bool)
|
||||
switch component.subtitle {
|
||||
case let .presence(presence):
|
||||
if let presence {
|
||||
labelData = stringAndActivityForUserPresence(strings: component.strings, dateTimeFormat: PresentationDateTimeFormat(), presence: presence, relativeTo: Int32(Date().timeIntervalSince1970))
|
||||
} else {
|
||||
labelData = (component.strings.LastSeen_Offline, false)
|
||||
}
|
||||
case let .text(text, icon):
|
||||
switch icon {
|
||||
case .lock:
|
||||
labelIcon = PresentationResourcesItemList.peerStatusLockedImage(component.theme)
|
||||
}
|
||||
labelData = (text, false)
|
||||
}
|
||||
|
||||
var maxTextSize = availableSize.width - leftInset - rightInset
|
||||
if labelIcon != nil {
|
||||
maxTextSize -= 48.0
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: labelData.0, font: Font.regular(15.0), textColor: labelData.1 ? component.theme.list.itemAccentColor : component.theme.list.itemSecondaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextSize, height: 100.0)
|
||||
)
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextSize, height: 100.0)
|
||||
)
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let centralContentHeight: CGFloat = titleSize.height + labelSize.height + titleSpacing
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
|
||||
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||
previousTitleContents?.removeFromSuperview()
|
||||
})
|
||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
if let labelIcon {
|
||||
let labelIconView: UIImageView
|
||||
if let current = self.labelIconView {
|
||||
labelIconView = current
|
||||
} else {
|
||||
labelIconView = UIImageView()
|
||||
self.labelIconView = labelIconView
|
||||
self.containerButton.addSubview(labelIconView)
|
||||
}
|
||||
labelIconView.image = labelIcon
|
||||
|
||||
let labelIconFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - 48.0 + floor((48.0 - labelIcon.size.width) * 0.5), y: floor((height - verticalInset * 2.0 - labelIcon.size.height) / 2.0)), size: CGSize(width: labelIcon.size.width, height: labelIcon.size.height))
|
||||
transition.setFrame(view: labelIconView, frame: labelIconFrame)
|
||||
} else {
|
||||
if let labelIconView = self.labelIconView {
|
||||
self.labelIconView = nil
|
||||
labelIconView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
}
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||
self.separatorLayer.isHidden = !component.hasNext
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
*/
|
@ -0,0 +1,274 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import TelegramStringFormatting
|
||||
import ListSectionComponent
|
||||
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
|
||||
private func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for gesture in gestureRecognizers {
|
||||
if let gesture = gesture as? ContextGesture {
|
||||
gesture.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
final class AdminUserActionsPeerComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let peer: EnginePeer?
|
||||
let selectionState: SelectionState
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
sideInset: CGFloat,
|
||||
title: String,
|
||||
peer: EnginePeer?,
|
||||
selectionState: SelectionState,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.sideInset = sideInset
|
||||
self.title = title
|
||||
self.peer = peer
|
||||
self.selectionState = selectionState
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: AdminUserActionsPeerComponent, rhs: AdminUserActionsPeerComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.sideInset != rhs.sideInset {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.peer != rhs.peer {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, ListSectionComponent.ChildView {
|
||||
private let containerButton: HighlightTrackingButton
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var labelIconView: UIImageView?
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var component: AdminUserActionsPeerComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public var separatorInset: CGFloat = 0.0
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.containerButton = HighlightTrackingButton()
|
||||
|
||||
self.avatarNode = AvatarNode(font: avatarFont)
|
||||
self.avatarNode.isLayerBacked = true
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.containerButton)
|
||||
self.containerButton.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.containerButton.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer)
|
||||
}
|
||||
|
||||
func update(component: AdminUserActionsPeerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var hasSelectionUpdated = false
|
||||
if let previousComponent = self.component {
|
||||
switch previousComponent.selectionState {
|
||||
case .none:
|
||||
if case .none = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
case .editing:
|
||||
if case .editing = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let contextInset: CGFloat = 0.0
|
||||
|
||||
let height: CGFloat = 44.0
|
||||
let verticalInset: CGFloat = 1.0
|
||||
let leftInset: CGFloat = 30.0 + component.sideInset
|
||||
var rightInset: CGFloat = contextInset * 2.0 + 8.0 + component.sideInset
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
if case let .editing(isSelected) = component.selectionState {
|
||||
rightInset += 46.0
|
||||
avatarLeftInset += 24.0
|
||||
|
||||
let checkSize: CGFloat = 22.0
|
||||
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
if themeUpdated {
|
||||
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
self.checkLayer = checkLayer
|
||||
self.containerButton.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
}
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: floor((22.0 - checkSize) * 0.5), y: floor((height - verticalInset * 2.0 - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||
checkLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let avatarSize: CGFloat = 30.0
|
||||
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - verticalInset * 2.0 - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
if let peer = component.peer {
|
||||
let clipStyle: AvatarNodeClipStyle
|
||||
if case let .channel(channel) = peer, channel.flags.contains(.isForum) {
|
||||
clipStyle = .roundedRect
|
||||
} else {
|
||||
clipStyle = .round
|
||||
}
|
||||
if peer.id == component.context.account.peerId {
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, overrideImage: .savedMessagesIcon, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
} else {
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, clipStyle: clipStyle, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
}
|
||||
|
||||
let avatarTitleSpacing: CGFloat = 5.0
|
||||
let maxTextSize = availableSize.width - avatarLeftInset - avatarSize - avatarTitleSpacing - rightInset
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: maxTextSize, height: 100.0)
|
||||
)
|
||||
|
||||
let centralContentHeight: CGFloat = titleSize.height
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: avatarLeftInset + avatarSize + avatarTitleSpacing, y: floor((height - verticalInset * 2.0 - centralContentHeight) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
|
||||
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||
previousTitleContents?.removeFromSuperview()
|
||||
})
|
||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
let containerFrame = CGRect(origin: CGPoint(x: contextInset, y: verticalInset), size: CGSize(width: availableSize.width - contextInset * 2.0, height: height - verticalInset * 2.0))
|
||||
transition.setFrame(view: self.containerButton, frame: containerFrame)
|
||||
|
||||
self.separatorInset = leftInset
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1199,6 +1199,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
}
|
||||
replyInfoNode.updateTouchesAtPoint(translatedPoint)
|
||||
}
|
||||
if let forwardInfoNode = strongSelf.forwardInfoNode {
|
||||
var translatedPoint: CGPoint?
|
||||
let convertedNodeFrame = forwardInfoNode.view.convert(forwardInfoNode.bounds, to: strongSelf.view)
|
||||
if let point = point, convertedNodeFrame.insetBy(dx: -4.0, dy: -4.0).contains(point) {
|
||||
translatedPoint = strongSelf.view.convert(point, to: forwardInfoNode.view)
|
||||
}
|
||||
forwardInfoNode.updateTouchesAtPoint(translatedPoint)
|
||||
}
|
||||
for contentNode in strongSelf.contentNodes {
|
||||
var translatedPoint: CGPoint?
|
||||
let convertedNodeFrame = contentNode.view.convert(contentNode.bounds, to: strongSelf.view)
|
||||
@ -2312,6 +2320,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
let sizeAndApply = forwardInfoLayout(item.context, item.presentationData, item.presentationData.strings, .bubble(incoming: incoming), forwardSource, forwardAuthorSignature, forwardPsaType, nil, CGSize(width: maximumNodeWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
forwardInfoSizeApply = (sizeAndApply.0, { width in sizeAndApply.1(width) })
|
||||
|
||||
headerSize.height += 2.0
|
||||
forwardInfoOriginY = headerSize.height
|
||||
headerSize.width = max(headerSize.width, forwardInfoSizeApply.0.width + bubbleWidthInsets)
|
||||
headerSize.height += forwardInfoSizeApply.0.height
|
||||
@ -2341,6 +2350,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
if storyType != .regular {
|
||||
headerSize.height += 6.0
|
||||
} else {
|
||||
headerSize.height += 2.0
|
||||
}
|
||||
|
||||
forwardInfoOriginY = headerSize.height
|
||||
@ -2349,6 +2360,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
if storyType != .regular {
|
||||
headerSize.height += 16.0
|
||||
} else {
|
||||
headerSize.height += 2.0
|
||||
}
|
||||
}
|
||||
|
||||
@ -4425,7 +4438,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
item.controllerInteraction.openPeer(EnginePeer(peer), peer is TelegramUser ? .info(nil) : .chat(textInputState: nil, subject: nil, peekData: nil), nil, .default)
|
||||
} else if let _ = forwardInfo.authorSignature {
|
||||
var subRect: CGRect?
|
||||
if let textNode = forwardInfoNode.textNode {
|
||||
if let textNode = forwardInfoNode.nameNode {
|
||||
subRect = textNode.frame
|
||||
}
|
||||
item.controllerInteraction.displayMessageTooltip(item.message.id, item.presentationData.strings.Conversation_ForwardAuthorHiddenTooltip, forwardInfoNode, subRect)
|
||||
|
@ -25,6 +25,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
|
||||
"//submodules/AvatarNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -7,6 +7,7 @@ import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import LocalizedPeerData
|
||||
import AccountContext
|
||||
import AvatarNode
|
||||
|
||||
public enum ChatMessageForwardInfoType: Equatable {
|
||||
case bubble(incoming: Bool)
|
||||
@ -73,10 +74,16 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public private(set) var textNode: TextNode?
|
||||
public private(set) var titleNode: TextNode?
|
||||
public private(set) var nameNode: TextNode?
|
||||
private var credibilityIconNode: ASImageNode?
|
||||
private var infoNode: InfoButtonNode?
|
||||
private var expiredStoryIconView: UIImageView?
|
||||
private var avatarNode: AvatarNode?
|
||||
|
||||
private var theme: PresentationTheme?
|
||||
private var highlightColor: UIColor?
|
||||
private var linkHighlightingNode: LinkHighlightingNode?
|
||||
|
||||
public var openPsa: ((String, ASDisplayNode) -> Void)?
|
||||
|
||||
@ -107,11 +114,61 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
public func updateTouchesAtPoint(_ point: CGPoint?) {
|
||||
var isHighlighted = false
|
||||
if point != nil {
|
||||
isHighlighted = true
|
||||
}
|
||||
|
||||
var initialRects: [CGRect] = []
|
||||
let addRects: (TextNode, CGPoint, CGFloat) -> Void = { textNode, offset, additionalWidth in
|
||||
guard let cachedLayout = textNode.cachedLayout else {
|
||||
return
|
||||
}
|
||||
for rect in cachedLayout.linesRects() {
|
||||
var rect = rect
|
||||
rect.size.width += rect.origin.x + additionalWidth
|
||||
rect.origin.x = 0.0
|
||||
initialRects.append(rect.offsetBy(dx: offset.x, dy: offset.y))
|
||||
}
|
||||
}
|
||||
|
||||
let offsetY: CGFloat = -12.0
|
||||
if let titleNode = self.titleNode {
|
||||
addRects(titleNode, CGPoint(x: titleNode.frame.minX, y: offsetY + titleNode.frame.minY), 0.0)
|
||||
|
||||
if let nameNode = self.nameNode {
|
||||
addRects(nameNode, CGPoint(x: titleNode.frame.minX, y: offsetY + nameNode.frame.minY), nameNode.frame.minX - titleNode.frame.minX)
|
||||
}
|
||||
}
|
||||
|
||||
if isHighlighted, !initialRects.isEmpty, let highlightColor = self.highlightColor {
|
||||
let rects = initialRects
|
||||
|
||||
let linkHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.linkHighlightingNode {
|
||||
linkHighlightingNode = current
|
||||
} else {
|
||||
linkHighlightingNode = LinkHighlightingNode(color: highlightColor)
|
||||
self.linkHighlightingNode = linkHighlightingNode
|
||||
self.addSubnode(linkHighlightingNode)
|
||||
}
|
||||
linkHighlightingNode.frame = self.bounds
|
||||
linkHighlightingNode.updateRects(rects)
|
||||
} else if let linkHighlightingNode = self.linkHighlightingNode {
|
||||
self.linkHighlightingNode = nil
|
||||
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
|
||||
linkHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public static func asyncLayout(_ maybeNode: ChatMessageForwardInfoNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ strings: PresentationStrings, _ type: ChatMessageForwardInfoType, _ peer: Peer?, _ authorName: String?, _ psaType: String?, _ storyData: StoryData?, _ constrainedSize: CGSize) -> (CGSize, (CGFloat) -> ChatMessageForwardInfoNode) {
|
||||
let textNodeLayout = TextNode.asyncLayout(maybeNode?.textNode)
|
||||
let titleNodeLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
let nameNodeLayout = TextNode.asyncLayout(maybeNode?.nameNode)
|
||||
|
||||
return { context, presentationData, strings, type, peer, authorName, psaType, storyData, constrainedSize in
|
||||
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 13.0 / 17.0)
|
||||
let fontSize = floor(presentationData.fontSize.baseDisplaySize * 14.0 / 17.0)
|
||||
let prefixFont = Font.regular(fontSize)
|
||||
let peerFont = Font.medium(fontSize)
|
||||
|
||||
@ -134,87 +191,94 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
let titleColor: UIColor
|
||||
let completeSourceString: PresentationStrings.FormattedString
|
||||
let titleString: PresentationStrings.FormattedString
|
||||
var authorString: String?
|
||||
|
||||
switch type {
|
||||
case let .bubble(incoming):
|
||||
if let psaType = psaType {
|
||||
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
|
||||
|
||||
var customFormat: String?
|
||||
let key = "Message.ForwardedPsa.\(psaType)"
|
||||
if let string = presentationData.strings.primaryComponent.dict[key] {
|
||||
customFormat = string
|
||||
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
||||
customFormat = string
|
||||
}
|
||||
|
||||
if let customFormat = customFormat {
|
||||
if let range = customFormat.range(of: "%@") {
|
||||
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
||||
let rightPart = String(customFormat[range.upperBound...])
|
||||
|
||||
let formattedText = leftPart + peerString + rightPart
|
||||
completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
||||
} else {
|
||||
completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
||||
}
|
||||
} else {
|
||||
completeSourceString = strings.Message_GenericForwardedPsa(peerString)
|
||||
}
|
||||
} else {
|
||||
if incoming {
|
||||
if let nameColor = peer?.nameColor {
|
||||
titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main
|
||||
} else {
|
||||
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
||||
}
|
||||
} else {
|
||||
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
}
|
||||
|
||||
if let storyData = storyData {
|
||||
switch storyData.storyType {
|
||||
case .regular:
|
||||
completeSourceString = strings.Message_ForwardedStoryShort(peerString)
|
||||
case .expired:
|
||||
completeSourceString = strings.Message_ForwardedExpiredStoryShort(peerString)
|
||||
case .unavailable:
|
||||
completeSourceString = strings.Message_ForwardedUnavailableStoryShort(peerString)
|
||||
}
|
||||
} else {
|
||||
completeSourceString = strings.Message_ForwardedMessageShort(peerString)
|
||||
}
|
||||
}
|
||||
case .standalone:
|
||||
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
titleColor = serviceColor.primaryText
|
||||
case let .bubble(incoming):
|
||||
if let psaType = psaType {
|
||||
titleColor = incoming ? presentationData.theme.theme.chat.message.incoming.polls.barPositive : presentationData.theme.theme.chat.message.outgoing.polls.barPositive
|
||||
|
||||
if let psaType = psaType {
|
||||
var customFormat: String?
|
||||
let key = "Message.ForwardedPsa.\(psaType)"
|
||||
if let string = presentationData.strings.primaryComponent.dict[key] {
|
||||
customFormat = string
|
||||
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
||||
customFormat = string
|
||||
}
|
||||
|
||||
if let customFormat = customFormat {
|
||||
if let range = customFormat.range(of: "%@") {
|
||||
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
||||
let rightPart = String(customFormat[range.upperBound...])
|
||||
|
||||
let formattedText = leftPart + peerString + rightPart
|
||||
completeSourceString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
||||
} else {
|
||||
completeSourceString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
||||
}
|
||||
var customFormat: String?
|
||||
let key = "Message.ForwardedPsa.\(psaType)"
|
||||
if let string = presentationData.strings.primaryComponent.dict[key] {
|
||||
customFormat = string
|
||||
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
||||
customFormat = string
|
||||
}
|
||||
|
||||
if let customFormat = customFormat {
|
||||
if let range = customFormat.range(of: "%@") {
|
||||
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
||||
let rightPart = String(customFormat[range.upperBound...])
|
||||
|
||||
let formattedText = leftPart + peerString + rightPart
|
||||
titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
||||
} else {
|
||||
completeSourceString = strings.Message_GenericForwardedPsa(peerString)
|
||||
titleString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
||||
}
|
||||
} else {
|
||||
completeSourceString = strings.Message_ForwardedMessageShort(peerString)
|
||||
titleString = strings.Message_GenericForwardedPsa(peerString)
|
||||
}
|
||||
} else {
|
||||
if incoming {
|
||||
if let nameColor = peer?.nameColor {
|
||||
titleColor = context.peerNameColors.get(nameColor, dark: presentationData.theme.theme.overallDarkAppearance).main
|
||||
} else {
|
||||
titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor
|
||||
}
|
||||
} else {
|
||||
titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
if let storyData = storyData {
|
||||
switch storyData.storyType {
|
||||
case .regular:
|
||||
titleString = PresentationStrings.FormattedString(string: "Forwarded story from", ranges: [])
|
||||
authorString = peerString
|
||||
case .expired:
|
||||
titleString = PresentationStrings.FormattedString(string: "Expired story from", ranges: [])
|
||||
authorString = peerString
|
||||
case .unavailable:
|
||||
titleString = PresentationStrings.FormattedString(string: "Expired story from", ranges: [])
|
||||
authorString = peerString
|
||||
}
|
||||
} else {
|
||||
titleString = PresentationStrings.FormattedString(string: "Forwarded from", ranges: [])
|
||||
authorString = peerString
|
||||
}
|
||||
}
|
||||
case .standalone:
|
||||
let serviceColor = serviceMessageColorComponents(theme: presentationData.theme.theme, wallpaper: presentationData.theme.wallpaper)
|
||||
titleColor = serviceColor.primaryText
|
||||
|
||||
if let psaType = psaType {
|
||||
var customFormat: String?
|
||||
let key = "Message.ForwardedPsa.\(psaType)"
|
||||
if let string = presentationData.strings.primaryComponent.dict[key] {
|
||||
customFormat = string
|
||||
} else if let string = presentationData.strings.secondaryComponent?.dict[key] {
|
||||
customFormat = string
|
||||
}
|
||||
|
||||
if let customFormat = customFormat {
|
||||
if let range = customFormat.range(of: "%@") {
|
||||
let leftPart = String(customFormat[customFormat.startIndex ..< range.lowerBound])
|
||||
let rightPart = String(customFormat[range.upperBound...])
|
||||
|
||||
let formattedText = leftPart + peerString + rightPart
|
||||
titleString = PresentationStrings.FormattedString(string: formattedText, ranges: [PresentationStrings.FormattedString.Range(index: 0, range: NSRange(location: leftPart.count, length: peerString.count))])
|
||||
} else {
|
||||
titleString = PresentationStrings.FormattedString(string: customFormat, ranges: [])
|
||||
}
|
||||
} else {
|
||||
titleString = strings.Message_GenericForwardedPsa(peerString)
|
||||
}
|
||||
} else {
|
||||
titleString = PresentationStrings.FormattedString(string: "Forwarded from", ranges: [])
|
||||
authorString = peerString
|
||||
}
|
||||
}
|
||||
|
||||
var currentCredibilityIconImage: UIImage?
|
||||
@ -230,17 +294,17 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
|
||||
if peer.isFake {
|
||||
switch type {
|
||||
case let .bubble(incoming):
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
||||
case .standalone:
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
||||
case let .bubble(incoming):
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
||||
case .standalone:
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.fakeIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
||||
}
|
||||
} else if peer.isScam {
|
||||
switch type {
|
||||
case let .bubble(incoming):
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
||||
case .standalone:
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
||||
case let .bubble(incoming):
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: incoming ? .regular : .outgoing)
|
||||
case .standalone:
|
||||
currentCredibilityIconImage = PresentationResourcesChatList.scamIcon(presentationData.theme.theme, strings: presentationData.strings, type: .service)
|
||||
}
|
||||
} else {
|
||||
currentCredibilityIconImage = nil
|
||||
@ -249,10 +313,9 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
highlight = false
|
||||
}
|
||||
|
||||
//let completeString: NSString = (completeSourceString.string.replacingOccurrences(of: "\n", with: " \n")) as NSString
|
||||
let completeString: NSString = completeSourceString.string as NSString
|
||||
let string = NSMutableAttributedString(string: completeString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont])
|
||||
if highlight, let range = completeSourceString.ranges.first?.range {
|
||||
let rawTitleString: NSString = titleString.string as NSString
|
||||
let string = NSMutableAttributedString(string: rawTitleString as String, attributes: [NSAttributedString.Key.foregroundColor: titleColor, NSAttributedString.Key.font: prefixFont])
|
||||
if highlight, let range = titleString.ranges.first?.range {
|
||||
string.addAttributes([NSAttributedString.Key.font: peerFont], range: range)
|
||||
}
|
||||
|
||||
@ -278,9 +341,34 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
let (textLayout, textApply) = textNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets()))
|
||||
let (titleLayout, titleApply) = titleNodeLayout(TextNodeLayoutArguments(attributedString: string, backgroundColor: nil, maximumNumberOfLines: 2, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: cutout, insets: UIEdgeInsets()))
|
||||
|
||||
return (CGSize(width: textLayout.size.width + credibilityIconWidth + infoWidth, height: textLayout.size.height), { width in
|
||||
var nameLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
if let authorString {
|
||||
nameLayoutAndApply = nameNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: authorString, font: peerFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: constrainedSize.width - credibilityIconWidth - infoWidth, height: constrainedSize.height), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
let titleAuthorSpacing: CGFloat = 0.0
|
||||
|
||||
var authorAvatarInset: CGFloat = 0.0
|
||||
if peer != nil {
|
||||
authorAvatarInset = 19.0
|
||||
}
|
||||
|
||||
let resultSize: CGSize
|
||||
if let nameLayoutAndApply {
|
||||
resultSize = CGSize(
|
||||
width: max(
|
||||
titleLayout.size.width + credibilityIconWidth + infoWidth,
|
||||
authorAvatarInset + nameLayoutAndApply.0.size.width
|
||||
),
|
||||
height: titleLayout.size.height + titleAuthorSpacing + nameLayoutAndApply.0.size.height
|
||||
)
|
||||
} else {
|
||||
resultSize = CGSize(width: titleLayout.size.width + credibilityIconWidth + infoWidth, height: titleLayout.size.height)
|
||||
}
|
||||
|
||||
return (resultSize, { width in
|
||||
let node: ChatMessageForwardInfoNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
@ -288,15 +376,57 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
node = ChatMessageForwardInfoNode()
|
||||
}
|
||||
|
||||
let textNode = textApply()
|
||||
textNode.displaysAsynchronously = !presentationData.isPreview
|
||||
node.theme = presentationData.theme.theme
|
||||
node.highlightColor = titleColor.withMultipliedAlpha(0.1)
|
||||
|
||||
if node.textNode == nil {
|
||||
textNode.isUserInteractionEnabled = false
|
||||
node.textNode = textNode
|
||||
node.addSubnode(textNode)
|
||||
let titleNode = titleApply()
|
||||
titleNode.displaysAsynchronously = !presentationData.isPreview
|
||||
|
||||
if node.titleNode == nil {
|
||||
titleNode.isUserInteractionEnabled = false
|
||||
node.titleNode = titleNode
|
||||
node.addSubnode(titleNode)
|
||||
}
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: titleLayout.size)
|
||||
|
||||
if let (nameLayout, nameApply) = nameLayoutAndApply {
|
||||
let nameNode = nameApply()
|
||||
if node.nameNode == nil {
|
||||
nameNode.isUserInteractionEnabled = false
|
||||
node.nameNode = nameNode
|
||||
node.addSubnode(nameNode)
|
||||
}
|
||||
nameNode.frame = CGRect(origin: CGPoint(x: leftOffset + authorAvatarInset, y: titleLayout.size.height + titleAuthorSpacing), size: nameLayout.size)
|
||||
|
||||
if let peer, authorAvatarInset != 0.0 {
|
||||
let avatarNode: AvatarNode
|
||||
if let current = node.avatarNode {
|
||||
avatarNode = current
|
||||
} else {
|
||||
avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 11.0))
|
||||
node.avatarNode = avatarNode
|
||||
node.addSubnode(avatarNode)
|
||||
}
|
||||
let avatarSize = CGSize(width: 16.0, height: 16.0)
|
||||
avatarNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: titleLayout.size.height + titleAuthorSpacing), size: avatarSize)
|
||||
avatarNode.updateSize(size: avatarSize)
|
||||
avatarNode.setPeer(context: context, theme: presentationData.theme.theme, peer: EnginePeer(peer), displayDimensions: avatarSize)
|
||||
} else {
|
||||
if let avatarNode = node.avatarNode {
|
||||
node.avatarNode = nil
|
||||
avatarNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let nameNode = node.nameNode {
|
||||
node.nameNode = nil
|
||||
nameNode.removeFromSupernode()
|
||||
}
|
||||
if let avatarNode = node.avatarNode {
|
||||
node.avatarNode = nil
|
||||
avatarNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
textNode.frame = CGRect(origin: CGPoint(x: leftOffset, y: 0.0), size: textLayout.size)
|
||||
|
||||
if let storyData, case .expired = storyData.storyType {
|
||||
let expiredStoryIconView: UIImageView
|
||||
@ -334,7 +464,7 @@ public class ChatMessageForwardInfoNode: ASDisplayNode {
|
||||
node.credibilityIconNode = credibilityIconNode
|
||||
node.addSubnode(credibilityIconNode)
|
||||
}
|
||||
credibilityIconNode.frame = CGRect(origin: CGPoint(x: textLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size)
|
||||
credibilityIconNode.frame = CGRect(origin: CGPoint(x: titleLayout.size.width + 4.0, y: 16.0), size: credibilityIconImage.size)
|
||||
credibilityIconNode.image = credibilityIconImage
|
||||
} else {
|
||||
node.credibilityIconNode?.removeFromSupernode()
|
||||
|
@ -26,6 +26,7 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/Chat/ChatMessageItemCommon",
|
||||
"//submodules/TelegramUI/Components/Chat/PollBubbleTimerNode",
|
||||
"//submodules/TelegramUI/Components/Chat/MergedAvatarsNode",
|
||||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -16,6 +16,7 @@ import ChatMessageBubbleContentNode
|
||||
import ChatMessageItemCommon
|
||||
import PollBubbleTimerNode
|
||||
import MergedAvatarsNode
|
||||
import TextNodeWithEntities
|
||||
|
||||
private final class ChatMessagePollOptionRadioNodeParameters: NSObject {
|
||||
let timestamp: Double
|
||||
@ -386,7 +387,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
private(set) var radioNode: ChatMessagePollOptionRadioNode?
|
||||
private let percentageNode: ASDisplayNode
|
||||
private var percentageImage: UIImage?
|
||||
private var titleNode: TextNode?
|
||||
private var titleNode: TextNodeWithEntities?
|
||||
private let buttonNode: HighlightTrackingButtonNode
|
||||
let separatorNode: ASDisplayNode
|
||||
private let resultBarNode: ASImageNode
|
||||
@ -400,6 +401,20 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
|
||||
weak var previousOptionNode: ChatMessagePollOptionNode?
|
||||
|
||||
var visibilityRect: CGRect? {
|
||||
didSet {
|
||||
if self.visibilityRect != oldValue {
|
||||
if let titleNode = self.titleNode {
|
||||
if let visibilityRect = self.visibilityRect {
|
||||
titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNode.textNode.frame.minY)
|
||||
} else {
|
||||
titleNode.visibilityRect = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.alpha = 0.0
|
||||
@ -476,19 +491,35 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode))) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(maybeNode?.titleNode)
|
||||
static func asyncLayout(_ maybeNode: ChatMessagePollOptionNode?) -> (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode))) {
|
||||
let makeTitleLayout = TextNodeWithEntities.asyncLayout(maybeNode?.titleNode)
|
||||
let currentResult = maybeNode?.currentResult
|
||||
let currentSelection = maybeNode?.currentSelection
|
||||
let currentTheme = maybeNode?.theme
|
||||
|
||||
return { accountPeerId, presentationData, message, poll, option, optionResult, constrainedWidth in
|
||||
return { context, presentationData, message, poll, option, optionResult, constrainedWidth in
|
||||
let leftInset: CGFloat = 50.0
|
||||
let rightInset: CGFloat = 12.0
|
||||
|
||||
let incoming = message.effectivelyIncoming(accountPeerId)
|
||||
let incoming = message.effectivelyIncoming(context.account.peerId)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: option.text, font: presentationData.messageFont, textColor: incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor), backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
||||
let optionTextColor: UIColor = incoming ? presentationData.theme.theme.chat.message.incoming.primaryTextColor : presentationData.theme.theme.chat.message.outgoing.primaryTextColor
|
||||
let optionAttributedText = stringWithAppliedEntities(
|
||||
option.text,
|
||||
entities: option.entities,
|
||||
baseColor: optionTextColor,
|
||||
linkColor: optionTextColor,
|
||||
baseFont: presentationData.messageFont,
|
||||
linkFont: presentationData.messageFont,
|
||||
boldFont: presentationData.messageFont,
|
||||
italicFont: presentationData.messageFont,
|
||||
boldItalicFont: presentationData.messageFont,
|
||||
fixedFont: presentationData.messageFont,
|
||||
blockQuoteFont: presentationData.messageFont,
|
||||
message: message
|
||||
)
|
||||
|
||||
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: optionAttributedText, backgroundColor: nil, maximumNumberOfLines: 0, truncationType: .end, constrainedSize: CGSize(width: max(1.0, constrainedWidth - leftInset - rightInset), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets(top: 1.0, left: 0.0, bottom: 1.0, right: 0.0)))
|
||||
|
||||
let contentHeight: CGFloat = max(46.0, titleLayout.size.height + 22.0)
|
||||
|
||||
@ -578,7 +609,7 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
return (titleLayout.size.width + leftInset + rightInset, { width in
|
||||
return (CGSize(width: width, height: contentHeight), { animated, inProgress in
|
||||
return (CGSize(width: width, height: contentHeight), { animated, inProgress, attemptSynchronous in
|
||||
let node: ChatMessagePollOptionNode
|
||||
if let maybeNode = maybeNode {
|
||||
node = maybeNode
|
||||
@ -596,17 +627,29 @@ private final class ChatMessagePollOptionNode: ASDisplayNode {
|
||||
|
||||
node.buttonNode.accessibilityLabel = option.text
|
||||
|
||||
let titleNode = titleApply()
|
||||
let titleNode = titleApply(TextNodeWithEntities.Arguments(
|
||||
context: context,
|
||||
cache: context.animationCache,
|
||||
renderer: context.animationRenderer,
|
||||
placeholderColor: incoming ? presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
|
||||
attemptSynchronous: attemptSynchronous
|
||||
))
|
||||
let titleNodeFrame: CGRect
|
||||
if titleLayout.hasRTL {
|
||||
titleNodeFrame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size)
|
||||
} else {
|
||||
titleNodeFrame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
}
|
||||
if node.titleNode !== titleNode {
|
||||
node.titleNode = titleNode
|
||||
node.addSubnode(titleNode)
|
||||
titleNode.isUserInteractionEnabled = false
|
||||
}
|
||||
if titleLayout.hasRTL {
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: width - rightInset - titleLayout.size.width, y: 11.0), size: titleLayout.size)
|
||||
} else {
|
||||
titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 11.0), size: titleLayout.size)
|
||||
node.addSubnode(titleNode.textNode)
|
||||
titleNode.textNode.isUserInteractionEnabled = false
|
||||
|
||||
if let visibilityRect = node.visibilityRect {
|
||||
titleNode.visibilityRect = visibilityRect.offsetBy(dx: 0.0, dy: titleNodeFrame.minY)
|
||||
}
|
||||
}
|
||||
titleNode.textNode.frame = titleNodeFrame
|
||||
|
||||
if shouldHaveRadioNode {
|
||||
let radioNode: ChatMessagePollOptionRadioNode
|
||||
@ -773,7 +816,7 @@ private final class SolutionButtonNode: HighlightableButtonNode {
|
||||
}
|
||||
|
||||
public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
private let textNode: TextNode
|
||||
private let textNode: TextNodeWithEntities
|
||||
private let typeNode: TextNode
|
||||
private var timerNode: PollBubbleTimerNode?
|
||||
private let solutionButtonNode: SolutionButtonNode
|
||||
@ -792,12 +835,34 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return self.solutionButtonNode
|
||||
}
|
||||
|
||||
override public var visibility: ListViewItemNodeVisibility {
|
||||
didSet {
|
||||
if oldValue != self.visibility {
|
||||
switch self.visibility {
|
||||
case .none:
|
||||
self.textNode.visibilityRect = nil
|
||||
for optionNode in self.optionNodes {
|
||||
optionNode.visibilityRect = nil
|
||||
}
|
||||
case let .visible(_, subRect):
|
||||
var subRect = subRect
|
||||
subRect.origin.x = 0.0
|
||||
subRect.size.width = 10000.0
|
||||
self.textNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -self.textNode.textNode.frame.minY)
|
||||
for optionNode in self.optionNodes {
|
||||
optionNode.visibilityRect = subRect.offsetBy(dx: 0.0, dy: -optionNode.frame.minY)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required public init() {
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.contentMode = .topLeft
|
||||
self.textNode.contentsScale = UIScreenScale
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode = TextNodeWithEntities()
|
||||
self.textNode.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.textNode.contentMode = .topLeft
|
||||
self.textNode.textNode.contentsScale = UIScreenScale
|
||||
self.textNode.textNode.displaysAsynchronously = false
|
||||
|
||||
self.typeNode = TextNode()
|
||||
self.typeNode.isUserInteractionEnabled = false
|
||||
@ -844,7 +909,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.textNode)
|
||||
self.addSubnode(self.textNode.textNode)
|
||||
self.addSubnode(self.typeNode)
|
||||
self.addSubnode(self.avatarsNode)
|
||||
self.addSubnode(self.votersNode)
|
||||
@ -914,7 +979,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) {
|
||||
let makeTextLayout = TextNode.asyncLayout(self.textNode)
|
||||
let makeTextLayout = TextNodeWithEntities.asyncLayout(self.textNode)
|
||||
let makeTypeLayout = TextNode.asyncLayout(self.typeNode)
|
||||
let makeVotersLayout = TextNode.asyncLayout(self.votersNode)
|
||||
let makeSubmitInactiveTextLayout = TextNode.asyncLayout(self.buttonSubmitInactiveTextNode)
|
||||
@ -931,7 +996,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
var previousOptionNodeLayouts: [Data: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
|
||||
var previousOptionNodeLayouts: [Data: (_ contet: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))] = [:]
|
||||
for optionNode in self.optionNodes {
|
||||
if let option = optionNode.option {
|
||||
previousOptionNodeLayouts[option.opaqueIdentifier] = ChatMessagePollOptionNode.asyncLayout(optionNode)
|
||||
@ -1049,7 +1114,25 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
let messageTheme = incoming ? item.presentationData.theme.theme.chat.message.incoming : item.presentationData.theme.theme.chat.message.outgoing
|
||||
|
||||
let attributedText = NSAttributedString(string: poll?.text ?? "", font: item.presentationData.messageBoldFont, textColor: messageTheme.primaryTextColor)
|
||||
let attributedText: NSAttributedString
|
||||
if let poll {
|
||||
attributedText = stringWithAppliedEntities(
|
||||
poll.text,
|
||||
entities: poll.textEntities,
|
||||
baseColor: messageTheme.primaryTextColor,
|
||||
linkColor: messageTheme.linkTextColor,
|
||||
baseFont: item.presentationData.messageBoldFont,
|
||||
linkFont: item.presentationData.messageBoldFont,
|
||||
boldFont: item.presentationData.messageBoldFont,
|
||||
italicFont: item.presentationData.messageBoldFont,
|
||||
boldItalicFont: item.presentationData.messageBoldFont,
|
||||
fixedFont: item.presentationData.messageBoldFont,
|
||||
blockQuoteFont: item.presentationData.messageBoldFont,
|
||||
message: message
|
||||
)
|
||||
} else {
|
||||
attributedText = NSAttributedString(string: "", font: item.presentationData.messageBoldFont, textColor: messageTheme.primaryTextColor)
|
||||
}
|
||||
|
||||
let textInsets = UIEdgeInsets(top: 2.0, left: 0.0, bottom: 5.0, right: 0.0)
|
||||
|
||||
@ -1144,7 +1227,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
isClosed = false
|
||||
}
|
||||
|
||||
var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
||||
var pollOptionsFinalizeLayouts: [(CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
||||
if let poll = poll {
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
var maxOptionVoterCount: Int32 = 0
|
||||
@ -1186,7 +1269,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
for i in 0 ..< poll.options.count {
|
||||
let option = poll.options[i]
|
||||
|
||||
let makeLayout: (_ accountPeerId: PeerId, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)))
|
||||
let makeLayout: (_ context: AccountContext, _ presentationData: ChatPresentationData, _ message: Message, _ poll: TelegramMediaPoll, _ option: TelegramMediaPollOption, _ optionResult: ChatMessagePollOptionResult?, _ constrainedWidth: CGFloat) -> (minimumWidth: CGFloat, layout: ((CGFloat) -> (CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)))
|
||||
if let previous = previousOptionNodeLayouts[option.opaqueIdentifier] {
|
||||
makeLayout = previous
|
||||
} else {
|
||||
@ -1202,7 +1285,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
} else if isClosed {
|
||||
optionResult = ChatMessagePollOptionResult(normalized: 0, percent: 0, count: 0)
|
||||
}
|
||||
let result = makeLayout(item.context.account.peerId, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
|
||||
let result = makeLayout(item.context, item.presentationData, item.message, poll, option, optionResult, constrainedSize.width - layoutConstants.bubble.borderInset * 2.0)
|
||||
boundingSize.width = max(boundingSize.width, result.minimumWidth + layoutConstants.bubble.borderInset * 2.0)
|
||||
pollOptionsFinalizeLayouts.append(result.1)
|
||||
}
|
||||
@ -1233,7 +1316,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
let typeOptionsSpacing: CGFloat = 3.0
|
||||
resultSize.height += titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
||||
|
||||
var optionNodesSizesAndApply: [(CGSize, (Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
||||
var optionNodesSizesAndApply: [(CGSize, (Bool, Bool, Bool) -> ChatMessagePollOptionNode)] = []
|
||||
for finalizeLayout in pollOptionsFinalizeLayouts {
|
||||
let result = finalizeLayout(boundingWidth - layoutConstants.bubble.borderInset * 2.0)
|
||||
resultSize.width = max(resultSize.width, result.0.width + layoutConstants.bubble.borderInset * 2.0)
|
||||
@ -1268,28 +1351,34 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.item = item
|
||||
strongSelf.poll = poll
|
||||
|
||||
let cachedLayout = strongSelf.textNode.cachedLayout
|
||||
let cachedLayout = strongSelf.textNode.textNode.cachedLayout
|
||||
|
||||
if case .System = animation {
|
||||
if let cachedLayout = cachedLayout {
|
||||
if cachedLayout != textLayout {
|
||||
if let textContents = strongSelf.textNode.contents {
|
||||
if let textContents = strongSelf.textNode.textNode.contents {
|
||||
let fadeNode = ASDisplayNode()
|
||||
fadeNode.displaysAsynchronously = false
|
||||
fadeNode.contents = textContents
|
||||
fadeNode.frame = strongSelf.textNode.frame
|
||||
fadeNode.frame = strongSelf.textNode.textNode.frame
|
||||
fadeNode.isLayerBacked = true
|
||||
strongSelf.addSubnode(fadeNode)
|
||||
fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in
|
||||
fadeNode?.removeFromSupernode()
|
||||
})
|
||||
strongSelf.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
strongSelf.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = textApply()
|
||||
let _ = textApply(TextNodeWithEntities.Arguments(
|
||||
context: item.context,
|
||||
cache: item.context.animationCache,
|
||||
renderer: item.context.animationRenderer,
|
||||
placeholderColor: incoming ? item.presentationData.theme.theme.chat.message.incoming.mediaPlaceholderColor : item.presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor,
|
||||
attemptSynchronous: synchronousLoad)
|
||||
)
|
||||
let _ = typeApply()
|
||||
|
||||
var verticalOffset = textFrame.maxY + titleTypeSpacing + typeLayout.size.height + typeOptionsSpacing
|
||||
@ -1302,7 +1391,7 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
isRequesting = inProgressOpaqueIds.contains(poll.options[i].opaqueIdentifier)
|
||||
}
|
||||
}
|
||||
let optionNode = apply(animation.isAnimated, isRequesting)
|
||||
let optionNode = apply(animation.isAnimated, isRequesting, synchronousLoad)
|
||||
if optionNode.supernode !== strongSelf {
|
||||
strongSelf.addSubnode(optionNode)
|
||||
let option = optionNode.option
|
||||
@ -1337,9 +1426,9 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
strongSelf.optionNodes = updatedOptionNodes
|
||||
|
||||
if textLayout.hasRTL {
|
||||
strongSelf.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
|
||||
strongSelf.textNode.textNode.frame = CGRect(origin: CGPoint(x: resultSize.width - textFrame.size.width - textInsets.left - layoutConstants.text.bubbleInsets.right - additionalTextRightInset, y: textFrame.origin.y), size: textFrame.size)
|
||||
} else {
|
||||
strongSelf.textNode.frame = textFrame
|
||||
strongSelf.textNode.textNode.frame = textFrame
|
||||
}
|
||||
let typeFrame = CGRect(origin: CGPoint(x: textFrame.minX, y: textFrame.maxY + titleTypeSpacing), size: typeLayout.size)
|
||||
strongSelf.typeNode.frame = typeFrame
|
||||
@ -1599,26 +1688,26 @@ public class ChatMessagePollBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
|
||||
override public func animateInsertion(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
override public func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
||||
self.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction {
|
||||
let textNodeFrame = self.textNode.frame
|
||||
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
let textNodeFrame = self.textNode.textNode.frame
|
||||
if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
|
||||
if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String {
|
||||
var concealed = true
|
||||
if let (attributeText, fullText) = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
|
||||
concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText)
|
||||
}
|
||||
return ChatMessageBubbleContentTapAction(content: .url(ChatMessageBubbleContentTapAction.Url(url: url, concealed: concealed)))
|
||||
|
@ -160,7 +160,18 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
return hasPremium
|
||||
}
|
||||
|
||||
public static func inputData(context: AccountContext, chatPeerId: PeerId?, areCustomEmojiEnabled: Bool, hasEdit: Bool = false, hasTrending: Bool = true, hasSearch: Bool = true, hideBackground: Bool = false, sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?) -> Signal<InputData, NoError> {
|
||||
public static func inputData(
|
||||
context: AccountContext,
|
||||
chatPeerId: PeerId?,
|
||||
areCustomEmojiEnabled: Bool,
|
||||
hasEdit: Bool = false,
|
||||
hasTrending: Bool = true,
|
||||
hasSearch: Bool = true,
|
||||
hasStickers: Bool = true,
|
||||
hasGifs: Bool = true,
|
||||
hideBackground: Bool = false,
|
||||
sendGif: ((FileMediaReference, UIView, CGRect, Bool, Bool) -> Bool)?
|
||||
) -> Signal<InputData, NoError> {
|
||||
let animationCache = context.animationCache
|
||||
let animationRenderer = context.animationRenderer
|
||||
|
||||
@ -184,7 +195,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings
|
||||
|
||||
let stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: hasTrending, forceHasPremium: false, hasEdit: hasEdit, hideBackground: hideBackground)
|
||||
let stickerItems: Signal<EmojiPagerContentComponent?, NoError>
|
||||
if hasStickers {
|
||||
stickerItems = EmojiPagerContentComponent.stickerInputData(context: context, animationCache: animationCache, animationRenderer: animationRenderer, stickerNamespaces: stickerNamespaces, stickerOrderedItemListCollectionIds: stickerOrderedItemListCollectionIds, chatPeerId: chatPeerId, hasSearch: hasSearch, hasTrending: hasTrending, forceHasPremium: false, hasEdit: hasEdit, hideBackground: hideBackground)
|
||||
|> map(Optional.init)
|
||||
} else {
|
||||
stickerItems = .single(nil)
|
||||
}
|
||||
|
||||
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|
||||
|> map { appConfiguration -> [String] in
|
||||
@ -197,10 +214,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||||
switch animatedEmoji {
|
||||
let animatedEmojiStickers: Signal<[String: [StickerPackItem]], NoError>
|
||||
|
||||
if hasGifs {
|
||||
animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
||||
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
||||
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
||||
switch animatedEmoji {
|
||||
case let .result(_, items, _):
|
||||
for item in items {
|
||||
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
||||
@ -213,8 +233,11 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return animatedEmojiStickers
|
||||
}
|
||||
return animatedEmojiStickers
|
||||
} else {
|
||||
animatedEmojiStickers = .single([:])
|
||||
}
|
||||
|
||||
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
|
||||
@ -236,22 +259,27 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
)
|
||||
|
||||
// We are going to subscribe to the actual data when the view is loaded
|
||||
let gifItems: Signal<EntityKeyboardGifContent, NoError> = .single(EntityKeyboardGifContent(
|
||||
hasRecentGifs: true,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: .recent,
|
||||
items: [],
|
||||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchCategories: nil,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
))
|
||||
let gifItems: Signal<EntityKeyboardGifContent?, NoError>
|
||||
if hasGifs {
|
||||
gifItems = .single(EntityKeyboardGifContent(
|
||||
hasRecentGifs: true,
|
||||
component: GifPagerContentComponent(
|
||||
context: context,
|
||||
inputInteraction: gifInputInteraction,
|
||||
subject: .recent,
|
||||
items: [],
|
||||
isLoading: false,
|
||||
loadMoreToken: nil,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
searchCategories: nil,
|
||||
searchInitiallyHidden: true,
|
||||
searchState: .empty(hasResults: false),
|
||||
hideBackground: hideBackground
|
||||
)
|
||||
))
|
||||
} else {
|
||||
gifItems = .single(nil)
|
||||
}
|
||||
|
||||
return combineLatest(queue: .mainQueue(),
|
||||
emojiItems,
|
||||
|
@ -15,6 +15,7 @@ swift_library(
|
||||
"//submodules/TelegramPresentationData",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/SwitchNode",
|
||||
"//submodules/CheckNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -5,11 +5,13 @@ import ComponentFlow
|
||||
import TelegramPresentationData
|
||||
import ListSectionComponent
|
||||
import SwitchNode
|
||||
import CheckNode
|
||||
|
||||
public final class ListActionItemComponent: Component {
|
||||
public enum ToggleStyle {
|
||||
case regular
|
||||
case icons
|
||||
case lock
|
||||
}
|
||||
|
||||
public struct Toggle: Equatable {
|
||||
@ -42,10 +44,23 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CustomAccessory: Equatable {
|
||||
public var component: AnyComponentWithIdentity<Empty>
|
||||
public var insets: UIEdgeInsets
|
||||
public var isInteractive: Bool
|
||||
|
||||
public init(component: AnyComponentWithIdentity<Empty>, insets: UIEdgeInsets = UIEdgeInsets(), isInteractive: Bool = false) {
|
||||
self.component = component
|
||||
self.insets = insets
|
||||
self.isInteractive = isInteractive
|
||||
}
|
||||
}
|
||||
|
||||
public enum Accessory: Equatable {
|
||||
case arrow
|
||||
case toggle(Toggle)
|
||||
case activity
|
||||
case custom(CustomAccessory)
|
||||
}
|
||||
|
||||
public enum IconInsets: Equatable {
|
||||
@ -65,22 +80,57 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public enum LeftIcon: Equatable {
|
||||
public final class Check: Equatable {
|
||||
public let isSelected: Bool
|
||||
public let toggle: (() -> Void)?
|
||||
|
||||
public init(isSelected: Bool, toggle: (() -> Void)?) {
|
||||
self.isSelected = isSelected
|
||||
self.toggle = toggle
|
||||
}
|
||||
|
||||
public static func ==(lhs: Check, rhs: Check) -> Bool {
|
||||
if lhs === rhs {
|
||||
return true
|
||||
}
|
||||
if lhs.isSelected != rhs.isSelected {
|
||||
return false
|
||||
}
|
||||
if (lhs.toggle == nil) != (rhs.toggle == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
case check(Check)
|
||||
case custom(AnyComponentWithIdentity<Empty>)
|
||||
}
|
||||
|
||||
public enum Highlighting {
|
||||
case `default`
|
||||
case disabled
|
||||
}
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let title: AnyComponent<Empty>
|
||||
public let contentInsets: UIEdgeInsets
|
||||
public let leftIcon: AnyComponentWithIdentity<Empty>?
|
||||
public let leftIcon: LeftIcon?
|
||||
public let icon: Icon?
|
||||
public let accessory: Accessory?
|
||||
public let action: ((UIView) -> Void)?
|
||||
public let highlighting: Highlighting
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
title: AnyComponent<Empty>,
|
||||
contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0),
|
||||
leftIcon: AnyComponentWithIdentity<Empty>? = nil,
|
||||
leftIcon: LeftIcon? = nil,
|
||||
icon: Icon? = nil,
|
||||
accessory: Accessory? = .arrow,
|
||||
action: ((UIView) -> Void)?
|
||||
action: ((UIView) -> Void)?,
|
||||
highlighting: Highlighting = .default
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
@ -89,6 +139,7 @@ public final class ListActionItemComponent: Component {
|
||||
self.icon = icon
|
||||
self.accessory = accessory
|
||||
self.action = action
|
||||
self.highlighting = highlighting
|
||||
}
|
||||
|
||||
public static func ==(lhs: ListActionItemComponent, rhs: ListActionItemComponent) -> Bool {
|
||||
@ -113,18 +164,96 @@ public final class ListActionItemComponent: Component {
|
||||
if (lhs.action == nil) != (rhs.action == nil) {
|
||||
return false
|
||||
}
|
||||
if lhs.highlighting != rhs.highlighting {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private final class CheckView: HighlightTrackingButton {
|
||||
private var checkLayer: CheckLayer?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
var action: (() -> Void)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let self, self.bounds.width > 0.0 {
|
||||
let animateScale = true
|
||||
|
||||
let topScale: CGFloat = (self.bounds.width - 8.0) / self.bounds.width
|
||||
let maxScale: CGFloat = (self.bounds.width + 2.0) / self.bounds.width
|
||||
|
||||
if highlighted {
|
||||
self.layer.removeAnimation(forKey: "opacity")
|
||||
self.layer.removeAnimation(forKey: "transform.scale")
|
||||
|
||||
if animateScale {
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
transition.setScale(layer: self.layer, scale: topScale)
|
||||
}
|
||||
} else {
|
||||
if animateScale {
|
||||
let transition = Transition(animation: .none)
|
||||
transition.setScale(layer: self.layer, scale: 1.0)
|
||||
|
||||
self.layer.animateScale(from: topScale, to: maxScale, duration: 0.13, timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, removeOnCompletion: false, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.layer.animateScale(from: maxScale, to: 1.0, duration: 0.1, timingFunction: CAMediaTimingFunctionName.easeIn.rawValue)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
self.action?()
|
||||
}
|
||||
|
||||
func update(size: CGSize, theme: PresentationTheme, isSelected: Bool, transition: Transition) {
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: theme, style: .plain), content: .check)
|
||||
self.checkLayer = checkLayer
|
||||
self.layer.addSublayer(checkLayer)
|
||||
}
|
||||
|
||||
if self.theme !== theme {
|
||||
self.theme = theme
|
||||
|
||||
checkLayer.theme = CheckNodeTheme(theme: theme, style: .plain)
|
||||
}
|
||||
|
||||
checkLayer.frame = CGRect(origin: CGPoint(), size: size)
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: HighlightTrackingButton, ListSectionComponent.ChildView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private var leftIcon: ComponentView<Empty>?
|
||||
private var leftCheckView: CheckView?
|
||||
private var icon: ComponentView<Empty>?
|
||||
|
||||
private var arrowView: UIImageView?
|
||||
private var switchNode: SwitchNode?
|
||||
private var iconSwitchNode: IconSwitchNode?
|
||||
private var activityIndicatorView: UIActivityIndicatorView?
|
||||
private var customAccessoryView: ComponentView<Empty>?
|
||||
|
||||
private var component: ListActionItemComponent?
|
||||
|
||||
@ -147,6 +276,9 @@ public final class ListActionItemComponent: Component {
|
||||
guard let self, let component = self.component, component.action != nil else {
|
||||
return
|
||||
}
|
||||
if isHighlighted, component.highlighting == .disabled {
|
||||
return
|
||||
}
|
||||
if case .toggle = component.accessory, component.action == nil {
|
||||
return
|
||||
}
|
||||
@ -180,6 +312,9 @@ public final class ListActionItemComponent: Component {
|
||||
|
||||
let themeUpdated = component.theme !== previousComponent?.theme
|
||||
|
||||
var customAccessorySize: CGSize?
|
||||
var customAccessoryTransition: Transition = transition
|
||||
|
||||
var contentLeftInset: CGFloat = 16.0
|
||||
let contentRightInset: CGFloat
|
||||
switch component.accessory {
|
||||
@ -195,13 +330,41 @@ public final class ListActionItemComponent: Component {
|
||||
contentRightInset = 76.0
|
||||
case .activity:
|
||||
contentRightInset = 76.0
|
||||
case let .custom(customAccessory):
|
||||
if case let .custom(previousCustomAccessory) = previousComponent?.accessory, previousCustomAccessory.component.id != customAccessory.component.id {
|
||||
self.customAccessoryView?.view?.removeFromSuperview()
|
||||
self.customAccessoryView = nil
|
||||
}
|
||||
|
||||
let customAccessoryView: ComponentView<Empty>
|
||||
if let current = self.customAccessoryView {
|
||||
customAccessoryView = current
|
||||
} else {
|
||||
customAccessoryTransition = customAccessoryTransition.withAnimation(.none)
|
||||
customAccessoryView = ComponentView()
|
||||
self.customAccessoryView = customAccessoryView
|
||||
}
|
||||
|
||||
let customAccessorySizeValue = customAccessoryView.update(
|
||||
transition: customAccessoryTransition,
|
||||
component: customAccessory.component.component,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
customAccessorySize = customAccessorySizeValue
|
||||
contentRightInset = customAccessorySizeValue.width + customAccessory.insets.left + customAccessory.insets.right
|
||||
}
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
contentHeight += component.contentInsets.top
|
||||
|
||||
if component.leftIcon != nil {
|
||||
contentLeftInset += 46.0
|
||||
if let leftIcon = component.leftIcon {
|
||||
switch leftIcon {
|
||||
case .check:
|
||||
contentLeftInset += 46.0
|
||||
case .custom:
|
||||
contentLeftInset += 46.0
|
||||
}
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
@ -275,39 +438,103 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
|
||||
if let leftIconValue = component.leftIcon {
|
||||
if previousComponent?.leftIcon?.id != leftIconValue.id, let leftIcon = self.leftIcon {
|
||||
self.leftIcon = nil
|
||||
if let iconView = leftIcon.view {
|
||||
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
|
||||
iconView?.removeFromSuperview()
|
||||
switch leftIconValue {
|
||||
case let .check(check):
|
||||
if let leftIcon = self.leftIcon {
|
||||
self.leftIcon = nil
|
||||
if let iconView = leftIcon.view {
|
||||
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
|
||||
iconView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let leftCheckView: CheckView
|
||||
var animateIn = false
|
||||
if let current = self.leftCheckView {
|
||||
leftCheckView = current
|
||||
} else {
|
||||
animateIn = true
|
||||
leftCheckView = CheckView()
|
||||
self.leftCheckView = leftCheckView
|
||||
self.addSubview(leftCheckView)
|
||||
|
||||
leftCheckView.action = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if case let .check(check) = component.leftIcon {
|
||||
check.toggle?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
leftCheckView.isUserInteractionEnabled = check.toggle != nil
|
||||
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - checkSize.width) * 0.5), y: floor((contentHeight - checkSize.height) * 0.5)), size: checkSize)
|
||||
|
||||
if animateIn {
|
||||
leftCheckView.frame = CGRect(origin: CGPoint(x: -checkSize.width, y: self.bounds.height == 0.0 ? checkFrame.minY : floor((self.bounds.height - checkSize.height) * 0.5)), size: checkFrame.size)
|
||||
transition.setPosition(view: leftCheckView, position: checkFrame.center)
|
||||
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: .immediate)
|
||||
} else {
|
||||
transition.setPosition(view: leftCheckView, position: checkFrame.center)
|
||||
transition.setBounds(view: leftCheckView, bounds: CGRect(origin: CGPoint(), size: checkFrame.size))
|
||||
leftCheckView.update(size: checkFrame.size, theme: component.theme, isSelected: check.isSelected, transition: transition)
|
||||
}
|
||||
case let .custom(customLeftIcon):
|
||||
var resetLeftIcon = false
|
||||
if case let .custom(previousCustomLeftIcon) = previousComponent?.leftIcon {
|
||||
if previousCustomLeftIcon.id != customLeftIcon.id {
|
||||
resetLeftIcon = true
|
||||
}
|
||||
} else {
|
||||
resetLeftIcon = true
|
||||
}
|
||||
if resetLeftIcon {
|
||||
if let leftIcon = self.leftIcon {
|
||||
self.leftIcon = nil
|
||||
if let iconView = leftIcon.view {
|
||||
transition.setAlpha(view: iconView, alpha: 0.0, completion: { [weak iconView] _ in
|
||||
iconView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if let leftCheckView = self.leftCheckView {
|
||||
self.leftCheckView = nil
|
||||
transition.setAlpha(view: leftCheckView, alpha: 0.0, completion: { [weak leftCheckView] _ in
|
||||
leftCheckView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var leftIconTransition = transition
|
||||
let leftIcon: ComponentView<Empty>
|
||||
if let current = self.leftIcon {
|
||||
leftIcon = current
|
||||
} else {
|
||||
leftIconTransition = leftIconTransition.withAnimation(.none)
|
||||
leftIcon = ComponentView()
|
||||
self.leftIcon = leftIcon
|
||||
}
|
||||
|
||||
let leftIconSize = leftIcon.update(
|
||||
transition: leftIconTransition,
|
||||
component: leftIconValue.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
)
|
||||
let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize)
|
||||
if let leftIconView = leftIcon.view {
|
||||
if leftIconView.superview == nil {
|
||||
leftIconView.isUserInteractionEnabled = false
|
||||
self.addSubview(leftIconView)
|
||||
transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0)
|
||||
|
||||
var leftIconTransition = transition
|
||||
let leftIcon: ComponentView<Empty>
|
||||
if let current = self.leftIcon {
|
||||
leftIcon = current
|
||||
} else {
|
||||
leftIconTransition = leftIconTransition.withAnimation(.none)
|
||||
leftIcon = ComponentView()
|
||||
self.leftIcon = leftIcon
|
||||
}
|
||||
|
||||
let leftIconSize = leftIcon.update(
|
||||
transition: leftIconTransition,
|
||||
component: customLeftIcon.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: availableSize.height)
|
||||
)
|
||||
let leftIconFrame = CGRect(origin: CGPoint(x: floor((contentLeftInset - leftIconSize.width) * 0.5), y: floor((min(60.0, contentHeight) - leftIconSize.height) * 0.5)), size: leftIconSize)
|
||||
if let leftIconView = leftIcon.view {
|
||||
if leftIconView.superview == nil {
|
||||
leftIconView.isUserInteractionEnabled = false
|
||||
self.addSubview(leftIconView)
|
||||
transition.animateAlpha(view: leftIconView, from: 0.0, to: 1.0)
|
||||
}
|
||||
leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame)
|
||||
}
|
||||
leftIconTransition.setFrame(view: leftIconView, frame: leftIconFrame)
|
||||
}
|
||||
} else {
|
||||
if let leftIcon = self.leftIcon {
|
||||
@ -318,6 +545,12 @@ public final class ListActionItemComponent: Component {
|
||||
})
|
||||
}
|
||||
}
|
||||
if let leftCheckView = self.leftCheckView {
|
||||
self.leftCheckView = nil
|
||||
transition.setAlpha(view: leftCheckView, alpha: 0.0, completion: { [weak leftCheckView] _ in
|
||||
leftCheckView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if case .arrow = component.accessory {
|
||||
@ -385,7 +618,7 @@ public final class ListActionItemComponent: Component {
|
||||
let switchSize = CGSize(width: 51.0, height: 31.0)
|
||||
let switchFrame = CGRect(origin: CGPoint(x: availableSize.width - 16.0 - switchSize.width, y: floor((min(60.0, contentHeight) - switchSize.height) * 0.5)), size: switchSize)
|
||||
switchTransition.setFrame(view: switchNode.view, frame: switchFrame)
|
||||
case .icons:
|
||||
case .icons, .lock:
|
||||
let switchNode: IconSwitchNode
|
||||
var switchTransition = transition
|
||||
var updateSwitchTheme = themeUpdated
|
||||
@ -466,6 +699,24 @@ public final class ListActionItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if case let .custom(customAccessory) = component.accessory, let customAccessoryView = self.customAccessoryView, let customAccessorySize {
|
||||
let activityAccessoryFrame = CGRect(origin: CGPoint(x: availableSize.width - customAccessory.insets.right - customAccessorySize.width, y: floor((contentHeight - customAccessorySize.height) * 0.5)), size: customAccessorySize)
|
||||
if let customAccessoryComponentView = customAccessoryView.view {
|
||||
if customAccessoryComponentView.superview == nil {
|
||||
customAccessoryComponentView.layer.anchorPoint = CGPoint(x: 1.0, y: 0.0)
|
||||
self.addSubview(customAccessoryComponentView)
|
||||
}
|
||||
customAccessoryComponentView.isUserInteractionEnabled = customAccessory.isInteractive
|
||||
customAccessoryTransition.setPosition(view: customAccessoryComponentView, position: CGPoint(x: activityAccessoryFrame.maxX, y: activityAccessoryFrame.minY))
|
||||
customAccessoryTransition.setBounds(view: customAccessoryComponentView, bounds: CGRect(origin: CGPoint(), size: activityAccessoryFrame.size))
|
||||
}
|
||||
} else {
|
||||
if let customAccessoryView = self.customAccessoryView {
|
||||
self.customAccessoryView = nil
|
||||
customAccessoryView.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
self.separatorInset = contentLeftInset
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
|
@ -88,8 +88,6 @@ public final class ListSectionContentView: UIView {
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.layer.addSublayer(self.contentSeparatorContainerLayer)
|
||||
self.layer.addSublayer(self.contentHighlightContainerLayer)
|
||||
self.addSubview(self.contentItemContainerView)
|
||||
@ -132,9 +130,16 @@ public final class ListSectionContentView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
public func update(configuration: Configuration, width: CGFloat, readyItems: [ReadyItem], transition: Transition) -> UpdateResult {
|
||||
public func update(configuration: Configuration, width: CGFloat, leftInset: CGFloat, readyItems: [ReadyItem], transition: Transition) -> UpdateResult {
|
||||
self.configuration = configuration
|
||||
|
||||
switch configuration.background {
|
||||
case .all, .range:
|
||||
self.clipsToBounds = true
|
||||
case let .none(clipped):
|
||||
self.clipsToBounds = clipped
|
||||
}
|
||||
|
||||
let backgroundColor: UIColor
|
||||
if self.highlightedItemId != nil && configuration.extendsItemHighlightToSection {
|
||||
backgroundColor = configuration.theme.list.itemHighlightedBackgroundColor
|
||||
@ -149,9 +154,11 @@ public final class ListSectionContentView: UIView {
|
||||
let readyItem = readyItems[index]
|
||||
validItemIds.append(readyItem.id)
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: innerContentHeight), size: readyItem.size)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: leftInset, y: innerContentHeight), size: readyItem.size)
|
||||
if let itemComponentView = readyItem.itemView.contents.view {
|
||||
var isAdded = false
|
||||
if itemComponentView.superview == nil {
|
||||
isAdded = true
|
||||
readyItem.itemView.addSubview(itemComponentView)
|
||||
self.contentItemContainerView.addSubview(readyItem.itemView)
|
||||
self.contentSeparatorContainerLayer.addSublayer(readyItem.itemView.separatorLayer)
|
||||
@ -174,7 +181,26 @@ public final class ListSectionContentView: UIView {
|
||||
if let itemComponentView = itemComponentView as? ListSectionComponentChildView {
|
||||
separatorInset = itemComponentView.separatorInset
|
||||
}
|
||||
readyItem.transition.setFrame(view: readyItem.itemView, frame: itemFrame)
|
||||
|
||||
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel))
|
||||
|
||||
if isAdded && itemComponentView is ListSubSectionComponent.View {
|
||||
readyItem.itemView.frame = itemFrame
|
||||
readyItem.itemView.clipsToBounds = true
|
||||
readyItem.itemView.frame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY), size: CGSize(width: itemFrame.width, height: 0.0))
|
||||
let itemView = readyItem.itemView
|
||||
transition.setFrame(view: readyItem.itemView, frame: itemFrame, completion: { [weak itemView] completed in
|
||||
if completed {
|
||||
itemView?.clipsToBounds = false
|
||||
}
|
||||
})
|
||||
|
||||
readyItem.itemView.separatorLayer.frame = CGRect(origin: CGPoint(x: itemSeparatorFrame.minX, y: itemFrame.minY), size: CGSize(width: itemSeparatorFrame.width, height: 0.0))
|
||||
transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
|
||||
} else {
|
||||
readyItem.transition.setFrame(view: readyItem.itemView, frame: itemFrame)
|
||||
readyItem.transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
|
||||
}
|
||||
|
||||
let itemSeparatorTopOffset: CGFloat = index == 0 ? 0.0 : -UIScreenPixel
|
||||
let itemHighlightFrame = CGRect(origin: CGPoint(x: itemFrame.minX, y: itemFrame.minY + itemSeparatorTopOffset), size: CGSize(width: itemFrame.width, height: itemFrame.height - itemSeparatorTopOffset))
|
||||
@ -182,9 +208,6 @@ public final class ListSectionContentView: UIView {
|
||||
|
||||
readyItem.transition.setFrame(view: itemComponentView, frame: CGRect(origin: CGPoint(), size: itemFrame.size))
|
||||
|
||||
let itemSeparatorFrame = CGRect(origin: CGPoint(x: separatorInset, y: itemFrame.maxY - UIScreenPixel), size: CGSize(width: width - separatorInset, height: UIScreenPixel))
|
||||
readyItem.transition.setFrame(layer: readyItem.itemView.separatorLayer, frame: itemSeparatorFrame)
|
||||
|
||||
let separatorAlpha: CGFloat
|
||||
if configuration.displaySeparators {
|
||||
if index != readyItems.count - 1 {
|
||||
@ -206,6 +229,12 @@ public final class ListSectionContentView: UIView {
|
||||
if !validItemIds.contains(id) {
|
||||
removedItemIds.append(id)
|
||||
|
||||
if let itemComponentView = itemView.contents.view, itemComponentView is ListSubSectionComponent.View {
|
||||
itemView.clipsToBounds = true
|
||||
transition.setFrame(view: itemView, frame: CGRect(origin: itemView.frame.origin, size: CGSize(width: itemView.bounds.width, height: 0.0)))
|
||||
transition.setFrame(layer: itemView.separatorLayer, frame: CGRect(origin: CGPoint(x: itemView.separatorLayer.frame.minX, y: itemView.frame.minY), size: itemView.separatorLayer.bounds.size))
|
||||
}
|
||||
|
||||
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
|
||||
itemView?.removeFromSuperview()
|
||||
})
|
||||
@ -328,8 +357,6 @@ public final class ListSectionComponent: Component {
|
||||
private var header: ComponentView<Empty>?
|
||||
private var footer: ComponentView<Empty>?
|
||||
|
||||
private var highlightedItemId: AnyHashable?
|
||||
|
||||
private var component: ListSectionComponent?
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
@ -345,6 +372,10 @@ public final class ListSectionComponent: Component {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func itemView(id: AnyHashable) -> UIView? {
|
||||
return self.contentView.itemViews[id]?.contents.view
|
||||
}
|
||||
|
||||
func update(component: ListSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
@ -422,6 +453,7 @@ public final class ListSectionComponent: Component {
|
||||
background: component.background
|
||||
),
|
||||
width: availableSize.width,
|
||||
leftInset: 0.0,
|
||||
readyItems: readyItems,
|
||||
transition: transition
|
||||
)
|
||||
@ -483,3 +515,135 @@ public final class ListSectionComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class ListSubSectionComponent: Component {
|
||||
public typealias ChildView = ListSectionComponentChildView
|
||||
|
||||
public let theme: PresentationTheme
|
||||
public let leftInset: CGFloat
|
||||
public let items: [AnyComponentWithIdentity<Empty>]
|
||||
public let displaySeparators: Bool
|
||||
|
||||
public init(
|
||||
theme: PresentationTheme,
|
||||
leftInset: CGFloat,
|
||||
items: [AnyComponentWithIdentity<Empty>],
|
||||
displaySeparators: Bool = true
|
||||
) {
|
||||
self.theme = theme
|
||||
self.leftInset = leftInset
|
||||
self.items = items
|
||||
self.displaySeparators = displaySeparators
|
||||
}
|
||||
|
||||
public static func ==(lhs: ListSubSectionComponent, rhs: ListSubSectionComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.leftInset != rhs.leftInset {
|
||||
return false
|
||||
}
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.displaySeparators != rhs.displaySeparators {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView, ListSectionComponent.ChildView {
|
||||
private let contentView: ListSectionContentView
|
||||
|
||||
private var component: ListSubSectionComponent?
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public var separatorInset: CGFloat = 0.0
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
self.contentView = ListSectionContentView()
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
public func itemView(id: AnyHashable) -> UIView? {
|
||||
return self.contentView.itemViews[id]?.contents.view
|
||||
}
|
||||
|
||||
func update(component: ListSubSectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
var readyItems: [ListSectionContentView.ReadyItem] = []
|
||||
for i in 0 ..< component.items.count {
|
||||
let item = component.items[i]
|
||||
let itemId = item.id
|
||||
|
||||
let itemView: ListSectionContentView.ItemView
|
||||
var itemTransition = transition
|
||||
if let current = self.contentView.itemViews[itemId] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemTransition = itemTransition.withAnimation(.none)
|
||||
itemView = ListSectionContentView.ItemView()
|
||||
self.contentView.itemViews[itemId] = itemView
|
||||
itemView.contents.parentState = state
|
||||
}
|
||||
|
||||
let itemSize = itemView.contents.update(
|
||||
transition: itemTransition,
|
||||
component: item.component,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - component.leftInset, height: availableSize.height)
|
||||
)
|
||||
|
||||
readyItems.append(ListSectionContentView.ReadyItem(
|
||||
id: itemId,
|
||||
itemView: itemView,
|
||||
size: itemSize,
|
||||
transition: itemTransition
|
||||
))
|
||||
}
|
||||
|
||||
let contentResult = self.contentView.update(
|
||||
configuration: ListSectionContentView.Configuration(
|
||||
theme: component.theme,
|
||||
displaySeparators: component.displaySeparators,
|
||||
extendsItemHighlightToSection: false,
|
||||
background: .none(clipped: false)
|
||||
),
|
||||
width: availableSize.width - component.leftInset,
|
||||
leftInset: 0.0,
|
||||
readyItems: readyItems,
|
||||
transition: transition
|
||||
)
|
||||
let innerContentHeight = contentResult.size.height
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: component.leftInset, y: contentHeight), size: CGSize(width: availableSize.width - component.leftInset, height: innerContentHeight))
|
||||
transition.setFrame(view: self.contentView, frame: contentFrame)
|
||||
transition.setFrame(view: self.contentView.externalContentBackgroundView, frame: contentResult.backgroundFrame.offsetBy(dx: contentFrame.minX, dy: contentFrame.minY))
|
||||
|
||||
contentHeight += innerContentHeight
|
||||
|
||||
self.separatorInset = component.leftInset
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,6 +168,10 @@ public final class PlainButtonComponent: Component {
|
||||
}
|
||||
|
||||
override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
if self.isHidden || self.alpha == 0.0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result != nil {
|
||||
return result
|
||||
|
@ -853,10 +853,10 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/ComposeIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -929,11 +929,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: !isSelected ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -1199,11 +1199,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -1229,11 +1229,11 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -1277,10 +1277,10 @@ final class AutomaticBusinessMessageSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/AddIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
|
@ -413,10 +413,10 @@ final class BusinessLinksSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Item List/AddLinkIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
|
@ -528,10 +528,10 @@ final class BusinessDaySetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Item List/AddTimeIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
|
@ -422,10 +422,10 @@ final class BusinessRecipientListScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
|
||||
name: "Chat List/AddIcon",
|
||||
tintColor: environment.theme.list.itemAccentColor
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
|
@ -770,11 +770,11 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: !self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
@ -800,11 +800,11 @@ final class ChatbotSetupScreenComponent: Component {
|
||||
maximumNumberOfLines: 1
|
||||
))),
|
||||
], alignment: .left, spacing: 2.0)),
|
||||
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
leftIcon: .custom(AnyComponentWithIdentity(id: 0, component: AnyComponent(Image(
|
||||
image: checkIcon,
|
||||
tintColor: self.hasAccessToAllChatsByDefault ? .clear : environment.theme.list.itemAccentColor,
|
||||
contentMode: .center
|
||||
))),
|
||||
)))),
|
||||
accessory: nil,
|
||||
action: { [weak self] _ in
|
||||
guard let self else {
|
||||
|
@ -1047,7 +1047,6 @@ public final class TextFieldComponent: Component {
|
||||
if let current = component.externalState.currentEmojiSuggestion, current.position.value == emojiSuggestionPosition.value {
|
||||
emojiSuggestion = current
|
||||
} else {
|
||||
|
||||
emojiSuggestion = EmojiSuggestion(localPosition: trackingPosition, position: emojiSuggestionPosition)
|
||||
component.externalState.currentEmojiSuggestion = emojiSuggestion
|
||||
}
|
||||
|
@ -9041,6 +9041,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let strongSelf = self, !actions.options.isEmpty {
|
||||
if let banAuthor = actions.banAuthor {
|
||||
strongSelf.presentBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, author: banAuthor, messageIds: messageIds, options: actions.options)
|
||||
} else if !actions.banAuthors.isEmpty {
|
||||
strongSelf.presentMultiBanMessageOptions(accountPeerId: strongSelf.context.account.peerId, authors: actions.banAuthors, messageIds: messageIds, options: actions.options)
|
||||
} else {
|
||||
if actions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty {
|
||||
strongSelf.presentClearCacheSuggestion()
|
||||
@ -13864,7 +13866,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
pollId: MediaId(namespace: Namespaces.Media.LocalPoll, id: Int64.random(in: Int64.min ... Int64.max)),
|
||||
publicity: poll.publicity,
|
||||
kind: poll.kind,
|
||||
text: poll.text,
|
||||
text: poll.text.string,
|
||||
textEntities: poll.text.entities,
|
||||
options: poll.options,
|
||||
correctAnswers: poll.correctAnswers,
|
||||
results: poll.results,
|
||||
@ -15650,298 +15653,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
|
||||
func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] participant in
|
||||
if let strongSelf = self {
|
||||
let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
var actions = Set<Int>([0])
|
||||
|
||||
let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in
|
||||
if actions.contains(category) {
|
||||
actions.remove(category)
|
||||
} else {
|
||||
actions.insert(category)
|
||||
}
|
||||
actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
||||
if let item = item as? ActionSheetCheckboxItem {
|
||||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
var itemIndex = 0
|
||||
var categories: [Int] = [0]
|
||||
if canBan {
|
||||
categories.append(1)
|
||||
}
|
||||
categories.append(contentsOf: [2, 3])
|
||||
|
||||
for categoryId in categories as [Int] {
|
||||
var title = ""
|
||||
if categoryId == 0 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Delete
|
||||
} else if categoryId == 1 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Ban
|
||||
} else if categoryId == 2 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Report
|
||||
} else if categoryId == 3 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
let index = itemIndex
|
||||
items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in
|
||||
toggleCheck(categoryId, index)
|
||||
}))
|
||||
itemIndex += 1
|
||||
}
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
if actions.contains(3) {
|
||||
let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone()
|
||||
let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone()
|
||||
} else if actions.contains(0) {
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
if actions.contains(1) {
|
||||
let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func presentDeleteMessageOptions(messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) {
|
||||
let _ = (self.context.engine.data.get(
|
||||
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
var personalPeerName: String?
|
||||
var isChannel = false
|
||||
if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
|
||||
personalPeerName = EnginePeer(user).compactDisplayTitle
|
||||
} else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser {
|
||||
personalPeerName = EnginePeer(user).compactDisplayTitle
|
||||
} else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
if options.contains(.cancelSending) {
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var contextItems: [ContextMenuItem] = []
|
||||
var canDisplayContextMenu = true
|
||||
|
||||
var unsendPersonalMessages = false
|
||||
if options.contains(.unsendPersonal) {
|
||||
canDisplayContextMenu = false
|
||||
items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").string))
|
||||
items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in
|
||||
unsendPersonalMessages = value
|
||||
}))
|
||||
} else if options.contains(.deleteGlobally) {
|
||||
let globalTitle: String
|
||||
if isChannel {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
} else if let personalPeerName = personalPeerName {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string
|
||||
} else {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in
|
||||
if let strongSelf = self {
|
||||
var giveaway: TelegramMediaGiveaway?
|
||||
for messageId in messageIds {
|
||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway {
|
||||
giveaway = media
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let commit = {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
if let giveaway {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if currentTime < giveaway.untilDate {
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings)
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: {
|
||||
commit()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
})], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
f(.default)
|
||||
} else {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
}
|
||||
} else {
|
||||
if "".isEmpty {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
} else {
|
||||
c.dismiss(completion: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
||||
commit()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
}))
|
||||
}
|
||||
if options.contains(.deleteLocally) {
|
||||
var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe
|
||||
if self.chatLocation.peerId == self.context.account.peerId {
|
||||
if case .peer(self.context.account.peerId) = self.chatLocation, messages.values.allSatisfy({ message in message?._asMessage().effectivelyIncoming(self.context.account.peerId) ?? false }) {
|
||||
localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages
|
||||
} else {
|
||||
localOptionText = self.presentationData.strings.Chat_ConfirmationDeleteFromSavedMessages
|
||||
}
|
||||
} else if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||||
localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete
|
||||
} else {
|
||||
if options.contains(.unsendPersonal) {
|
||||
localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count))
|
||||
} else if case .peer(self.context.account.peerId) = self.chatLocation {
|
||||
if messageIds.count == 1 {
|
||||
localOptionText = self.presentationData.strings.Conversation_Moderate_Delete
|
||||
} else {
|
||||
localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages
|
||||
}
|
||||
}
|
||||
}
|
||||
contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
|
||||
let commit: () -> Void = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
|
||||
}
|
||||
|
||||
if "".isEmpty {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
} else {
|
||||
c.dismiss(completion: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
||||
commit()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})))
|
||||
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
|
||||
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if canDisplayContextMenu, let contextController = contextController {
|
||||
contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true)
|
||||
} else {
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
|
||||
if let contextController = contextController {
|
||||
contextController.dismiss(completion: { [weak self] in
|
||||
self?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
} else {
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
completion(.default)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func presentClearCacheSuggestion() {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
|
||||
|
||||
var presented = false
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self, !presented {
|
||||
presented = true
|
||||
let context = strongSelf.context
|
||||
strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
|
||||
return storageUsageExceptionsScreen(context: context, category: category)
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.presentInGlobalOverlay(actionSheet)
|
||||
}
|
||||
|
||||
@available(iOSApplicationExtension 11.0, iOS 11.0, *)
|
||||
public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
|
||||
|
512
submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift
Normal file
512
submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift
Normal file
@ -0,0 +1,512 @@
|
||||
import Foundation
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import UndoUI
|
||||
import AdminUserActionsSheet
|
||||
import ContextUI
|
||||
import TelegramStringFormatting
|
||||
import StorageUsageScreen
|
||||
import SettingsUI
|
||||
import DeleteChatPeerActionSheetItem
|
||||
|
||||
fileprivate struct InitialBannedRights {
|
||||
var value: TelegramChatBannedRights?
|
||||
}
|
||||
|
||||
extension ChatControllerImpl {
|
||||
fileprivate func applyAdminUserActionsResult(messageIds: Set<MessageId>, result: AdminUserActionsSheet.Result, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
//TODO:localize
|
||||
let title: String = "Messages Deleted"
|
||||
var text: String = ""
|
||||
var undoRights: [EnginePeer.Id: InitialBannedRights] = [:]
|
||||
|
||||
if !result.reportSpamPeers.isEmpty {
|
||||
if !text.isEmpty {
|
||||
text.append("\n")
|
||||
}
|
||||
if result.reportSpamPeers.count == 1 {
|
||||
text.append("**1** user reported for spam")
|
||||
} else {
|
||||
text.append("**\(result.reportSpamPeers.count)** users reported for spam")
|
||||
}
|
||||
}
|
||||
if !result.banPeers.isEmpty {
|
||||
if !text.isEmpty {
|
||||
text.append("\n")
|
||||
}
|
||||
if result.banPeers.count == 1 {
|
||||
text.append("**1** user banned")
|
||||
} else {
|
||||
text.append("**\(result.banPeers.count)** users banned")
|
||||
}
|
||||
for id in result.banPeers {
|
||||
if let value = initialUserBannedRights[id] {
|
||||
undoRights[id] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if !result.updateBannedRights.isEmpty {
|
||||
if !text.isEmpty {
|
||||
text.append("\n")
|
||||
}
|
||||
if result.updateBannedRights.count == 1 {
|
||||
text.append("**1** user restricted")
|
||||
} else {
|
||||
text.append("**\(result.updateBannedRights.count)** users restricted")
|
||||
}
|
||||
for id in result.banPeers {
|
||||
if let value = initialUserBannedRights[id] {
|
||||
undoRights[id] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let _ = self.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
|
||||
for authorId in result.deleteAllFromPeers {
|
||||
let _ = self.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: authorId, namespace: Namespaces.Message.Cloud).startStandalone()
|
||||
let _ = self.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: authorId).startStandalone()
|
||||
}
|
||||
|
||||
for authorId in result.reportSpamPeers {
|
||||
let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone()
|
||||
}
|
||||
|
||||
for authorId in result.banPeers {
|
||||
let _ = self.context.engine.peers.removePeerMember(peerId: peerId, memberId: authorId).startStandalone()
|
||||
}
|
||||
|
||||
for (authorId, rights) in result.updateBannedRights {
|
||||
let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: authorId, rights: rights).startStandalone()
|
||||
}
|
||||
}
|
||||
|
||||
self.present(
|
||||
UndoOverlayController(
|
||||
presentationData: self.presentationData,
|
||||
content: undoRights.isEmpty ? .actionSucceeded(title: text.isEmpty ? nil : title, text: text.isEmpty ? title : text, cancel: nil, destructive: false) : .removedChat(title: title, text: text),
|
||||
elevatedLayout: false,
|
||||
action: { [weak self] action in
|
||||
guard let self else {
|
||||
return true
|
||||
}
|
||||
|
||||
switch action {
|
||||
case .commit:
|
||||
break
|
||||
case .undo:
|
||||
for (authorId, rights) in initialUserBannedRights {
|
||||
let _ = self.context.engine.peers.updateChannelMemberBannedRights(peerId: peerId, memberId: authorId, rights: rights.value).startStandalone()
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
return true
|
||||
}
|
||||
),
|
||||
in: .current
|
||||
)
|
||||
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
}
|
||||
|
||||
func presentMultiBanMessageOptions(accountPeerId: PeerId, authors: [Peer], messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
self.navigationActionDisposable.set((combineLatest(authors.map { author in
|
||||
self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id)
|
||||
})
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] participants in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let _ = (self.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { [weak self] chatPeer in
|
||||
guard let self, let chatPeer else {
|
||||
return
|
||||
}
|
||||
var peers: [EnginePeer] = []
|
||||
var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:]
|
||||
for maybeParticipant in participants {
|
||||
guard let participant = maybeParticipant else {
|
||||
continue
|
||||
}
|
||||
switch participant {
|
||||
case .creator:
|
||||
break
|
||||
case let .member(_, _, _, banInfo, _):
|
||||
if let banInfo {
|
||||
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights)
|
||||
} else {
|
||||
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
for author in authors {
|
||||
peers.append(EnginePeer(author))
|
||||
}
|
||||
self.push(AdminUserActionsSheet(
|
||||
context: self.context,
|
||||
chatPeer: chatPeer,
|
||||
peers: peers,
|
||||
messageCount: messageIds.count,
|
||||
completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
|
||||
}
|
||||
))
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
func presentBanMessageOptions(accountPeerId: PeerId, author: Peer, messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions) {
|
||||
guard let peerId = self.chatLocation.peerId else {
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
self.navigationActionDisposable.set((self.context.engine.peers.fetchChannelParticipant(peerId: peerId, participantId: author.id)
|
||||
|> deliverOnMainQueue).startStrict(next: { [weak self] participant in
|
||||
if let strongSelf = self {
|
||||
if "".isEmpty {
|
||||
let _ = (strongSelf.context.engine.data.get(
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: peerId),
|
||||
TelegramEngine.EngineData.Item.Peer.Peer(id: author.id)
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { chatPeer, authorPeer in
|
||||
guard let self, let chatPeer else {
|
||||
return
|
||||
}
|
||||
guard let authorPeer else {
|
||||
return
|
||||
}
|
||||
var initialUserBannedRights: [EnginePeer.Id: InitialBannedRights] = [:]
|
||||
if let participant {
|
||||
switch participant {
|
||||
case .creator:
|
||||
break
|
||||
case let .member(_, _, _, banInfo, _):
|
||||
if let banInfo {
|
||||
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: banInfo.rights)
|
||||
} else {
|
||||
initialUserBannedRights[participant.peerId] = InitialBannedRights(value: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
self.push(AdminUserActionsSheet(
|
||||
context: self.context,
|
||||
chatPeer: chatPeer,
|
||||
peers: [authorPeer],
|
||||
messageCount: messageIds.count,
|
||||
completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
|
||||
}
|
||||
))
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let canBan = participant?.canBeBannedBy(peerId: accountPeerId) ?? true
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
var actions = Set<Int>([0])
|
||||
|
||||
let toggleCheck: (Int, Int) -> Void = { [weak actionSheet] category, itemIndex in
|
||||
if actions.contains(category) {
|
||||
actions.remove(category)
|
||||
} else {
|
||||
actions.insert(category)
|
||||
}
|
||||
actionSheet?.updateItem(groupIndex: 0, itemIndex: itemIndex, { item in
|
||||
if let item = item as? ActionSheetCheckboxItem {
|
||||
return ActionSheetCheckboxItem(title: item.title, label: item.label, value: !item.value, action: item.action)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
var itemIndex = 0
|
||||
var categories: [Int] = [0]
|
||||
if canBan {
|
||||
categories.append(1)
|
||||
}
|
||||
categories.append(contentsOf: [2, 3])
|
||||
|
||||
for categoryId in categories as [Int] {
|
||||
var title = ""
|
||||
if categoryId == 0 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Delete
|
||||
} else if categoryId == 1 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Ban
|
||||
} else if categoryId == 2 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_Report
|
||||
} else if categoryId == 3 {
|
||||
title = strongSelf.presentationData.strings.Conversation_Moderate_DeleteAllMessages(EnginePeer(author).displayTitle(strings: strongSelf.presentationData.strings, displayOrder: strongSelf.presentationData.nameDisplayOrder)).string
|
||||
}
|
||||
let index = itemIndex
|
||||
items.append(ActionSheetCheckboxItem(title: title, label: "", value: actions.contains(categoryId), action: { value in
|
||||
toggleCheck(categoryId, index)
|
||||
}))
|
||||
itemIndex += 1
|
||||
}
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Done, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
if actions.contains(3) {
|
||||
let _ = strongSelf.context.engine.messages.deleteAllMessagesWithAuthor(peerId: peerId, authorId: author.id, namespace: Namespaces.Message.Cloud).startStandalone()
|
||||
let _ = strongSelf.context.engine.messages.clearAuthorHistory(peerId: peerId, memberId: author.id).startStandalone()
|
||||
} else if actions.contains(0) {
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
if actions.contains(1) {
|
||||
let _ = strongSelf.context.engine.peers.removePeerMember(peerId: peerId, memberId: author.id).startStandalone()
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
strongSelf.present(actionSheet, in: .window(.root))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
func presentDeleteMessageOptions(messageIds: Set<MessageId>, options: ChatAvailableMessageActionOptions, contextController: ContextControllerProtocol?, completion: @escaping (ContextMenuActionResult) -> Void) {
|
||||
let _ = (self.context.engine.data.get(
|
||||
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
var personalPeerName: String?
|
||||
var isChannel = false
|
||||
if let user = self.presentationInterfaceState.renderedPeer?.peer as? TelegramUser {
|
||||
personalPeerName = EnginePeer(user).compactDisplayTitle
|
||||
} else if let peer = self.presentationInterfaceState.renderedPeer?.peer as? TelegramSecretChat, let associatedPeerId = peer.associatedPeerId, let user = self.presentationInterfaceState.renderedPeer?.peers[associatedPeerId] as? TelegramUser {
|
||||
personalPeerName = EnginePeer(user).compactDisplayTitle
|
||||
} else if let channel = self.presentationInterfaceState.renderedPeer?.peer as? TelegramChannel, case .broadcast = channel.info {
|
||||
isChannel = true
|
||||
}
|
||||
|
||||
if options.contains(.cancelSending) {
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.Conversation_ContextMenuCancelSending, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
var contextItems: [ContextMenuItem] = []
|
||||
var canDisplayContextMenu = true
|
||||
|
||||
var unsendPersonalMessages = false
|
||||
if options.contains(.unsendPersonal) {
|
||||
canDisplayContextMenu = false
|
||||
items.append(ActionSheetTextItem(title: self.presentationData.strings.Chat_UnsendMyMessagesAlertTitle(personalPeerName ?? "").string))
|
||||
items.append(ActionSheetSwitchItem(title: self.presentationData.strings.Chat_UnsendMyMessages, isOn: false, action: { value in
|
||||
unsendPersonalMessages = value
|
||||
}))
|
||||
} else if options.contains(.deleteGlobally) {
|
||||
let globalTitle: String
|
||||
if isChannel {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
} else if let personalPeerName = personalPeerName {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesFor(personalPeerName).string
|
||||
} else {
|
||||
globalTitle = self.presentationData.strings.Conversation_DeleteMessagesForEveryone
|
||||
}
|
||||
contextItems.append(.action(ContextMenuActionItem(text: globalTitle, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in
|
||||
if let strongSelf = self {
|
||||
var giveaway: TelegramMediaGiveaway?
|
||||
for messageId in messageIds {
|
||||
if let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(messageId) {
|
||||
if let media = message.media.first(where: { $0 is TelegramMediaGiveaway }) as? TelegramMediaGiveaway {
|
||||
giveaway = media
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let commit = {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
if let giveaway {
|
||||
let currentTime = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
if currentTime < giveaway.untilDate {
|
||||
Queue.mainQueue().after(0.2) {
|
||||
let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings)
|
||||
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: {
|
||||
commit()
|
||||
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
|
||||
})], parseMarkdown: true), in: .window(.root))
|
||||
}
|
||||
f(.default)
|
||||
} else {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
}
|
||||
} else {
|
||||
if "".isEmpty {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
} else {
|
||||
c.dismiss(completion: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
||||
commit()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})))
|
||||
items.append(ActionSheetButtonItem(title: globalTitle, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: .forEveryone).startStandalone()
|
||||
}
|
||||
}))
|
||||
}
|
||||
if options.contains(.deleteLocally) {
|
||||
var localOptionText = self.presentationData.strings.Conversation_DeleteMessagesForMe
|
||||
if self.chatLocation.peerId == self.context.account.peerId {
|
||||
if case .peer(self.context.account.peerId) = self.chatLocation, messages.values.allSatisfy({ message in message?._asMessage().effectivelyIncoming(self.context.account.peerId) ?? false }) {
|
||||
localOptionText = self.presentationData.strings.Chat_ConfirmationRemoveFromSavedMessages
|
||||
} else {
|
||||
localOptionText = self.presentationData.strings.Chat_ConfirmationDeleteFromSavedMessages
|
||||
}
|
||||
} else if case .scheduledMessages = self.presentationInterfaceState.subject {
|
||||
localOptionText = messageIds.count > 1 ? self.presentationData.strings.ScheduledMessages_DeleteMany : self.presentationData.strings.ScheduledMessages_Delete
|
||||
} else {
|
||||
if options.contains(.unsendPersonal) {
|
||||
localOptionText = self.presentationData.strings.Chat_DeleteMessagesConfirmation(Int32(messageIds.count))
|
||||
} else if case .peer(self.context.account.peerId) = self.chatLocation {
|
||||
if messageIds.count == 1 {
|
||||
localOptionText = self.presentationData.strings.Conversation_Moderate_Delete
|
||||
} else {
|
||||
localOptionText = self.presentationData.strings.Conversation_DeleteManyMessages
|
||||
}
|
||||
}
|
||||
}
|
||||
contextItems.append(.action(ContextMenuActionItem(text: localOptionText, textColor: .destructive, icon: { _ in nil }, action: { [weak self] c, f in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
|
||||
let commit: () -> Void = {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
|
||||
}
|
||||
|
||||
if "".isEmpty {
|
||||
f(.dismissWithoutContent)
|
||||
commit()
|
||||
} else {
|
||||
c.dismiss(completion: {
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: {
|
||||
commit()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})))
|
||||
items.append(ActionSheetButtonItem(title: localOptionText, color: .destructive, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withoutSelectionState() } })
|
||||
let _ = strongSelf.context.engine.messages.deleteMessagesInteractively(messageIds: Array(messageIds), type: unsendPersonalMessages ? .forEveryone : .forLocalPeer).startStandalone()
|
||||
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if canDisplayContextMenu, let contextController = contextController {
|
||||
contextController.setItems(.single(ContextController.Items(content: .list(contextItems))), minHeight: nil, animated: true)
|
||||
} else {
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
|
||||
if let contextController = contextController {
|
||||
contextController.dismiss(completion: { [weak self] in
|
||||
self?.present(actionSheet, in: .window(.root))
|
||||
})
|
||||
} else {
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.present(actionSheet, in: .window(.root))
|
||||
completion(.default)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func presentClearCacheSuggestion() {
|
||||
guard let peer = self.presentationInterfaceState.renderedPeer?.peer else {
|
||||
return
|
||||
}
|
||||
self.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState({ $0.withoutSelectionState() }) })
|
||||
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData)
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(DeleteChatPeerActionSheetItem(context: self.context, peer: EnginePeer(peer), chatPeer: EnginePeer(peer), action: .clearCacheSuggestion, strings: self.presentationData.strings, nameDisplayOrder: self.presentationData.nameDisplayOrder))
|
||||
|
||||
var presented = false
|
||||
items.append(ActionSheetButtonItem(title: self.presentationData.strings.ClearCache_FreeSpace, color: .accent, action: { [weak self, weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
if let strongSelf = self, !presented {
|
||||
presented = true
|
||||
let context = strongSelf.context
|
||||
strongSelf.push(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
|
||||
return storageUsageExceptionsScreen(context: context, category: category)
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])])
|
||||
self.chatDisplayNode.dismissInput()
|
||||
self.presentInGlobalOverlay(actionSheet)
|
||||
}
|
||||
}
|
@ -456,7 +456,7 @@ class ChatControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
||||
}
|
||||
if let poll = media as? TelegramMediaPoll {
|
||||
var updatedMedia = message.media.filter { !($0 is TelegramMediaPoll) }
|
||||
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil))
|
||||
updatedMedia.append(TelegramMediaPoll(pollId: poll.pollId, publicity: poll.publicity, kind: poll.kind, text: poll.text, textEntities: poll.textEntities, options: poll.options, correctAnswers: poll.correctAnswers, results: TelegramMediaPollResults(voters: nil, totalVoters: nil, recentVoters: [], solution: nil), isClosed: false, deadlineTimeout: nil))
|
||||
messageMedia = updatedMedia
|
||||
}
|
||||
if let _ = media as? TelegramMediaDice {
|
||||
|
@ -881,6 +881,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
messageActions = ChatAvailableMessageActions(
|
||||
options: messageActions.options.intersection([.deleteLocally, .deleteGlobally, .forward]),
|
||||
banAuthor: nil,
|
||||
banAuthors: [],
|
||||
disableDelete: true,
|
||||
isCopyProtected: messageActions.isCopyProtected,
|
||||
setTag: false,
|
||||
@ -2066,6 +2067,7 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
|
||||
var optionsMap: [MessageId: ChatAvailableMessageActionOptions] = [:]
|
||||
var banPeer: Peer?
|
||||
var banPeers: [Peer] = []
|
||||
var hadPersonalIncoming = false
|
||||
var hadBanPeerId = false
|
||||
var disableDelete = false
|
||||
@ -2182,23 +2184,33 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
}
|
||||
if channel.hasPermission(.banMembers), case .group = channel.info {
|
||||
if message.flags.contains(.Incoming) {
|
||||
if message.author is TelegramUser {
|
||||
if !hadBanPeerId {
|
||||
if let author = message.author {
|
||||
if author is TelegramUser {
|
||||
if !hadBanPeerId {
|
||||
hadBanPeerId = true
|
||||
banPeer = author
|
||||
} else if banPeer?.id != message.author?.id {
|
||||
banPeer = nil
|
||||
}
|
||||
|
||||
if !banPeers.contains(where: { $0.id == author.id }) {
|
||||
banPeers.append(author)
|
||||
}
|
||||
} else if author is TelegramChannel {
|
||||
if !hadBanPeerId {
|
||||
hadBanPeerId = true
|
||||
banPeer = author
|
||||
} else if banPeer?.id != message.author?.id {
|
||||
banPeer = nil
|
||||
}
|
||||
|
||||
if !banPeers.contains(where: { $0.id == author.id }) {
|
||||
banPeers.append(author)
|
||||
}
|
||||
} else {
|
||||
hadBanPeerId = true
|
||||
banPeer = message.author
|
||||
} else if banPeer?.id != message.author?.id {
|
||||
banPeer = nil
|
||||
}
|
||||
} else if message.author is TelegramChannel {
|
||||
if !hadBanPeerId {
|
||||
hadBanPeerId = true
|
||||
banPeer = message.author
|
||||
} else if banPeer?.id != message.author?.id {
|
||||
banPeer = nil
|
||||
}
|
||||
} else {
|
||||
hadBanPeerId = true
|
||||
banPeer = nil
|
||||
}
|
||||
} else {
|
||||
hadBanPeerId = true
|
||||
@ -2338,9 +2350,9 @@ func chatAvailableMessageActionsImpl(engine: TelegramEngine, accountPeerId: Peer
|
||||
commonTags = nil
|
||||
}
|
||||
|
||||
return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, disableDelete: disableDelete, isCopyProtected: isCopyProtected, setTag: setTag, editTags: commonTags ?? Set())
|
||||
return ChatAvailableMessageActions(options: reducedOptions, banAuthor: banPeer, banAuthors: banPeers, disableDelete: disableDelete, isCopyProtected: isCopyProtected, setTag: setTag, editTags: commonTags ?? Set())
|
||||
} else {
|
||||
return ChatAvailableMessageActions(options: [], banAuthor: nil, disableDelete: false, isCopyProtected: isCopyProtected, setTag: false, editTags: Set())
|
||||
return ChatAvailableMessageActions(options: [], banAuthor: nil, banAuthors: [], disableDelete: false, isCopyProtected: isCopyProtected, setTag: false, editTags: Set())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user