- custom poll emoji input

- avatars in forward info
- ban user sheet
This commit is contained in:
Isaac 2024-04-16 23:42:39 +04:00
parent a9c8ae8595
commit 5123b841c3
39 changed files with 4240 additions and 789 deletions

View File

@ -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

View File

@ -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
}

View File

@ -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()

View File

@ -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",

View File

@ -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,

View File

@ -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),

View File

@ -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

View File

@ -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;
}

View File

@ -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))
}

View File

@ -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)

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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": [],

View 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",
],
)

View File

@ -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)
}
}
*/

View File

@ -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)
}
}

View File

@ -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)

View File

@ -25,6 +25,7 @@ swift_library(
"//submodules/TelegramUI/Components/TextNodeWithEntities",
"//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",

View File

@ -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()

View File

@ -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",

View File

@ -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)))

View File

@ -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,

View File

@ -15,6 +15,7 @@ swift_library(
"//submodules/TelegramPresentationData",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/SwitchNode",
"//submodules/CheckNode",
],
visibility = [
"//visibility:public",

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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 {

View File

@ -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
}

View File

@ -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 {

View 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)
}
}

View File

@ -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 {

View File

@ -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())
}
}
}