mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-06 17:00:13 +00:00
Various improvements
This commit is contained in:
parent
a835c0a6f5
commit
88405d99ec
@ -14428,11 +14428,18 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Notification.TodoCompleted" = "%1$@ marked \"%2$@\" as done.";
|
||||
"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone.";
|
||||
"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\".";
|
||||
"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\".";
|
||||
|
||||
"Notification.TodoMultipleCompleted" = "%1$@ marked %2$@ as done.";
|
||||
"Notification.TodoMultipleIncompleted" = "%1$@ marked %2$@ as undone.";
|
||||
"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done.";
|
||||
"Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not done.";
|
||||
"Notification.TodoMultipleCompletedYou" = "You marked %1$@ as done.";
|
||||
"Notification.TodoMultipleIncompletedYou" = "You marked %1$@ as not done.";
|
||||
|
||||
"Notification.TodoAddedTasks_1" = "%@ task";
|
||||
"Notification.TodoAddedTasks_any" = "%@ tasks";
|
||||
|
||||
"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\".";
|
||||
"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\".";
|
||||
"Notification.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\".";
|
||||
"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\".";
|
||||
|
||||
|
||||
@ -778,9 +778,11 @@ public enum ChatControllerSubject: Equatable {
|
||||
}
|
||||
|
||||
public var quote: Quote?
|
||||
public var todoTaskId: Int32?
|
||||
|
||||
public init(quote: Quote? = nil) {
|
||||
public init(quote: Quote? = nil, todoTaskId: Int32? = nil) {
|
||||
self.quote = quote
|
||||
self.todoTaskId = todoTaskId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ public struct MessageHistoryScrollToSubject: Equatable {
|
||||
public var todoTaskId: Int32?
|
||||
public var setupReply: Bool
|
||||
|
||||
public init(index: MessageHistoryAnchorIndex, quote: Quote?, todoTaskId: Int32? = nil, setupReply: Bool = false) {
|
||||
public init(index: MessageHistoryAnchorIndex, quote: Quote? = nil, todoTaskId: Int32? = nil, setupReply: Bool = false) {
|
||||
self.index = index
|
||||
self.quote = quote
|
||||
self.todoTaskId = todoTaskId
|
||||
@ -44,10 +44,12 @@ public struct MessageHistoryInitialSearchSubject: Equatable {
|
||||
|
||||
public var location: ChatHistoryInitialSearchLocation
|
||||
public var quote: Quote?
|
||||
public var todoTaskId: Int32?
|
||||
|
||||
public init(location: ChatHistoryInitialSearchLocation, quote: Quote?) {
|
||||
public init(location: ChatHistoryInitialSearchLocation, quote: Quote? = nil, todoTaskId: Int32? = nil) {
|
||||
self.location = location
|
||||
self.quote = quote
|
||||
self.todoTaskId = todoTaskId
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4755,7 +4755,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
||||
strongSelf.interaction.dismissInput()
|
||||
strongSelf.interaction.present(controller, nil)
|
||||
} else if case let .messages(chatLocation, _, _) = playlistLocation {
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(EngineMessage.Tags.music))
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(EngineMessage.Tags.music))
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -165,6 +165,9 @@ final class ComposePollScreenComponent: Component {
|
||||
|
||||
private var currentEditingTag: AnyObject?
|
||||
|
||||
private var reorderRecognizer: ReorderGestureRecognizer?
|
||||
private var reorderingItem: (id: AnyHashable, snapshotView: UIView, backgroundView: UIView, initialPosition: CGPoint, position: CGPoint)?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.scrollView = UIScrollView()
|
||||
self.scrollView.showsVerticalScrollIndicator = true
|
||||
@ -181,6 +184,39 @@ final class ComposePollScreenComponent: Component {
|
||||
|
||||
self.scrollView.delegate = self
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
let reorderRecognizer = ReorderGestureRecognizer(
|
||||
shouldBegin: { [weak self] point in
|
||||
guard let self, let (id, item) = self.item(at: point) else {
|
||||
return (allowed: false, requiresLongPress: false, id: nil, item: nil)
|
||||
}
|
||||
return (allowed: true, requiresLongPress: false, id: id, item: item)
|
||||
},
|
||||
willBegin: { point in
|
||||
},
|
||||
began: { [weak self] item in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: item)
|
||||
},
|
||||
ended: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: nil)
|
||||
},
|
||||
moved: { [weak self] distance in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.moveReorderingItem(distance: distance)
|
||||
},
|
||||
isActiveUpdated: { _ in
|
||||
}
|
||||
)
|
||||
self.reorderRecognizer = reorderRecognizer
|
||||
self.addGestureRecognizer(reorderRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
@ -195,6 +231,114 @@ final class ComposePollScreenComponent: Component {
|
||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
||||
}
|
||||
|
||||
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
||||
let localPoint = self.pollOptionsSectionContainer.convert(point, from: self)
|
||||
for (id, itemView) in self.pollOptionsSectionContainer.itemViews {
|
||||
if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed {
|
||||
let viewFrame = view.convert(view.bounds, to: self.pollOptionsSectionContainer)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height))
|
||||
if iconFrame.contains(localPoint) {
|
||||
return (id, itemView.contents)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setReorderingItem(item: AnyHashable?) {
|
||||
guard let environment = self.environment else {
|
||||
return
|
||||
}
|
||||
var mappedItem: (AnyHashable, ComponentView<Empty>)?
|
||||
for (id, itemView) in self.pollOptionsSectionContainer.itemViews {
|
||||
if id == item {
|
||||
mappedItem = (id, itemView.contents)
|
||||
break
|
||||
}
|
||||
}
|
||||
if self.reorderingItem?.id != mappedItem?.0 {
|
||||
if let (id, visibleItem) = mappedItem, let view = visibleItem.view, !view.isHidden, let viewSuperview = view.superview, let snapshotView = view.snapshotView(afterScreenUpdates: false) {
|
||||
let mappedCenter = viewSuperview.convert(view.center, to: self.scrollView)
|
||||
|
||||
let wrapperView = UIView()
|
||||
wrapperView.alpha = 0.8
|
||||
wrapperView.frame = CGRect(origin: mappedCenter.offsetBy(dx: -snapshotView.bounds.width / 2.0, dy: -snapshotView.bounds.height / 2.0), size: snapshotView.bounds.size)
|
||||
|
||||
let theme = environment.theme.withModalBlocksBackground()
|
||||
let backgroundView = UIImageView(image: generateReorderingBackgroundImage(backgroundColor: theme.list.itemBlocksBackgroundColor))
|
||||
backgroundView.frame = wrapperView.bounds.insetBy(dx: -10.0, dy: -10.0)
|
||||
snapshotView.frame = snapshotView.bounds
|
||||
|
||||
wrapperView.addSubview(backgroundView)
|
||||
wrapperView.addSubview(snapshotView)
|
||||
|
||||
backgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
wrapperView.transform = CGAffineTransformMakeScale(1.04, 1.04)
|
||||
wrapperView.layer.animateScale(from: 1.0, to: 1.04, duration: 0.2)
|
||||
|
||||
self.scrollView.addSubview(wrapperView)
|
||||
self.reorderingItem = (id, wrapperView, backgroundView, mappedCenter, mappedCenter)
|
||||
self.state?.updated()
|
||||
} else {
|
||||
if let reorderingItem = self.reorderingItem {
|
||||
self.reorderingItem = nil
|
||||
for (itemId, itemView) in self.pollOptionsSectionContainer.itemViews {
|
||||
if itemId == reorderingItem.id, let view = itemView.contents.view {
|
||||
let viewFrame = view.convert(view.bounds, to: self)
|
||||
let transition = ComponentTransition.spring(duration: 0.3)
|
||||
transition.setPosition(view: reorderingItem.snapshotView, position: viewFrame.center)
|
||||
transition.setAlpha(view: reorderingItem.backgroundView, alpha: 0.0, completion: { _ in
|
||||
reorderingItem.snapshotView.removeFromSuperview()
|
||||
self.state?.updated()
|
||||
})
|
||||
transition.setScale(view: reorderingItem.snapshotView, scale: 1.0)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveReorderingItem(distance: CGPoint) {
|
||||
if let (id, snapshotView, backgroundView, initialPosition, _) = self.reorderingItem {
|
||||
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
||||
self.reorderingItem = (id, snapshotView, backgroundView, initialPosition, targetPosition)
|
||||
|
||||
snapshotView.center = targetPosition
|
||||
|
||||
for (itemId, itemView) in self.pollOptionsSectionContainer.itemViews {
|
||||
if itemId == id {
|
||||
continue
|
||||
}
|
||||
if let view = itemView.contents.view {
|
||||
let viewFrame = view.convert(view.bounds, to: self)
|
||||
if viewFrame.contains(targetPosition) {
|
||||
if let targetIndex = self.pollOptions.firstIndex(where: { AnyHashable($0.id) == itemId }), let reorderingItem = self.pollOptions.first(where: { AnyHashable($0.id) == id }) {
|
||||
self.reorderIfPossible(item: reorderingItem, toIndex: targetIndex)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func reorderIfPossible(item: PollOption, toIndex: Int) {
|
||||
let targetItem = self.pollOptions[toIndex]
|
||||
guard targetItem.textInputState.hasText else {
|
||||
return
|
||||
}
|
||||
if let fromIndex = self.pollOptions.firstIndex(where: { $0.id == item.id }) {
|
||||
self.pollOptions[toIndex] = item
|
||||
self.pollOptions[fromIndex] = targetItem
|
||||
|
||||
HapticFeedback().tap()
|
||||
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
}
|
||||
}
|
||||
|
||||
func validatedInput() -> ComposedPoll? {
|
||||
if self.pollTextInputState.text.length == 0 {
|
||||
return nil
|
||||
@ -799,6 +943,7 @@ final class ComposePollScreenComponent: Component {
|
||||
},
|
||||
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
|
||||
characterLimit: component.initialData.maxPollOptionLength,
|
||||
canReorder: true,
|
||||
emptyLineHandling: .notAllowed,
|
||||
returnKeyAction: { [weak self] in
|
||||
guard let self else {
|
||||
@ -848,6 +993,13 @@ final class ComposePollScreenComponent: Component {
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
deleteAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.pollOptions.removeAll(where: { $0.id == optionId })
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
tag: pollOption.textFieldTag
|
||||
))))
|
||||
|
||||
@ -878,6 +1030,12 @@ final class ComposePollScreenComponent: Component {
|
||||
size: itemSize,
|
||||
transition: itemTransition
|
||||
))
|
||||
|
||||
var isReordering = false
|
||||
if let reorderingItem = self.reorderingItem, itemId == reorderingItem.id {
|
||||
isReordering = true
|
||||
}
|
||||
itemView.contents.view?.isHidden = isReordering
|
||||
}
|
||||
|
||||
for i in 0 ..< self.pollOptions.count {
|
||||
|
||||
@ -864,7 +864,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
strongSelf.displayNode.view.window?.endEditing(true)
|
||||
strongSelf.present(controller, in: .window(.root))
|
||||
} else if case let .messages(chatLocation, _, _) = playlistLocation {
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: chatLocation, subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -1298,7 +1298,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
}
|
||||
|
||||
var taskTitle = "DELETED"
|
||||
var taskTitle: String?
|
||||
if let todo {
|
||||
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
|
||||
taskTitle = completedTask.text
|
||||
@ -1306,16 +1306,20 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
taskTitle = incompletedTask.text
|
||||
}
|
||||
}
|
||||
if taskTitle.count > 20 {
|
||||
taskTitle = taskTitle.prefix(20) + "…"
|
||||
if let taskTitleValue = taskTitle, taskTitleValue.count > 20 {
|
||||
taskTitle = taskTitleValue.prefix(20) + "…"
|
||||
}
|
||||
|
||||
if message.author?.id == accountPeerId {
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if let _ = completed.first {
|
||||
resultString = strings.Notification_TodoCompletedYou(taskTitle)
|
||||
if completed.count > 1 || (completed.count == 1 && taskTitle == nil) {
|
||||
resultString = strings.Notification_TodoMultipleCompletedYou(strings.Notification_TodoTasks(Int32(completed.count)))
|
||||
} else if let _ = completed.first {
|
||||
resultString = strings.Notification_TodoCompletedYou(taskTitle ?? "")
|
||||
} else if incompleted.count > 1 || (incompleted.count == 1 && taskTitle == nil) {
|
||||
resultString = strings.Notification_TodoMultipleIncompletedYou(strings.Notification_TodoTasks(Int32(incompleted.count)))
|
||||
} else if let _ = incompleted.first {
|
||||
resultString = strings.Notification_TodoIncompletedYou(taskTitle)
|
||||
resultString = strings.Notification_TodoIncompletedYou(taskTitle ?? "")
|
||||
} else {
|
||||
resultString = strings.Notification_TodoCompletedYou("")
|
||||
}
|
||||
@ -1327,10 +1331,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
attributes[1] = boldAttributes
|
||||
|
||||
let resultString: PresentationStrings.FormattedString
|
||||
if let _ = completed.first {
|
||||
resultString = strings.Notification_TodoCompleted(peerName, taskTitle)
|
||||
if completed.count > 1 {
|
||||
resultString = strings.Notification_TodoMultipleCompleted(peerName, strings.Notification_TodoTasks(Int32(completed.count)))
|
||||
} else if let _ = completed.first {
|
||||
resultString = strings.Notification_TodoCompleted(peerName, taskTitle ?? "")
|
||||
} else if incompleted.count > 1 {
|
||||
resultString = strings.Notification_TodoMultipleIncompleted(peerName, strings.Notification_TodoTasks(Int32(incompleted.count)))
|
||||
} else if let _ = incompleted.first {
|
||||
resultString = strings.Notification_TodoIncompleted(peerName, taskTitle)
|
||||
resultString = strings.Notification_TodoIncompleted(peerName, taskTitle ?? "")
|
||||
} else {
|
||||
resultString = strings.Notification_TodoCompleted(peerName, "")
|
||||
}
|
||||
@ -1359,7 +1367,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
resultString = strings.Notification_TodoAddedTaskYou(taskTitle, todoTitle)
|
||||
} else {
|
||||
resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
|
||||
resultString = strings.Notification_TodoAddedMultipleTasksYou(strings.Notification_TodoAddedTasks(Int32(tasks.count)), todoTitle)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
|
||||
} else {
|
||||
@ -1376,7 +1384,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
||||
}
|
||||
resultString = strings.Notification_TodoAddedTask(peerName, taskTitle, todoTitle)
|
||||
} else {
|
||||
resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoTasks(Int32(tasks.count)), todoTitle)
|
||||
resultString = strings.Notification_TodoAddedMultipleTasks(peerName, strings.Notification_TodoAddedTasks(Int32(tasks.count)), todoTitle)
|
||||
}
|
||||
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||
}
|
||||
|
||||
@ -5916,8 +5916,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
for contentNode in self.contentNodes {
|
||||
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
|
||||
contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true)
|
||||
} else if let _ = contentNode as? ChatMessageTodoBubbleContentNode {
|
||||
|
||||
} else if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode {
|
||||
contentNode.updateTaskHighlightState(id: nil, color: .clear, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6012,8 +6012,19 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
||||
|
||||
var taskFrame: CGRect?
|
||||
for contentNode in self.contentNodes {
|
||||
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode, let localFrame = contentNode.taskItemFrame(id: todoTaskId) {
|
||||
taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview)
|
||||
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode {
|
||||
contentNode.updateTaskHighlightState(id: todoTaskId, color: highlightColor, animated: false)
|
||||
var sourceFrame = backgroundHighlightNode.view.convert(backgroundHighlightNode.bounds, to: contentNode.view)
|
||||
if item.message.effectivelyIncoming(item.context.account.peerId) {
|
||||
sourceFrame.origin.x += 6.0
|
||||
sourceFrame.size.width -= 6.0
|
||||
} else {
|
||||
sourceFrame.size.width -= 6.0
|
||||
}
|
||||
|
||||
if let localFrame = contentNode.animateTaskItemHighlightIn(id: todoTaskId, sourceFrame: sourceFrame, transition: transition) {
|
||||
taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview).insetBy(dx: -3.0, dy: 0.0)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -1242,10 +1242,73 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
return nil
|
||||
}
|
||||
|
||||
public func taskItemFrame(id: Int32) -> CGRect? {
|
||||
private var taskHighlightingNode: LinkHighlightingNode?
|
||||
public func updateTaskHighlightState(id: Int32?, color: UIColor, animated: Bool) {
|
||||
var rectsSet: [CGRect] = []
|
||||
for node in self.optionNodes {
|
||||
if node.option?.id == id {
|
||||
return node.frame
|
||||
rectsSet.append(node.frame.insetBy(dx: 3.0 - UIScreenPixel, dy: 2.0 - UIScreenPixel))
|
||||
}
|
||||
}
|
||||
if !rectsSet.isEmpty {
|
||||
let rects = rectsSet
|
||||
let taskHighlightingNode: LinkHighlightingNode
|
||||
if let current = self.taskHighlightingNode {
|
||||
taskHighlightingNode = current
|
||||
} else {
|
||||
taskHighlightingNode = LinkHighlightingNode(color: color)
|
||||
taskHighlightingNode.innerRadius = 0.0
|
||||
taskHighlightingNode.outerRadius = 0.0
|
||||
self.taskHighlightingNode = taskHighlightingNode
|
||||
self.insertSubnode(taskHighlightingNode, belowSubnode: self.buttonNode)
|
||||
}
|
||||
taskHighlightingNode.frame = self.bounds
|
||||
taskHighlightingNode.updateRects(rects)
|
||||
} else {
|
||||
if let taskHighlightingNode = self.taskHighlightingNode {
|
||||
self.taskHighlightingNode = nil
|
||||
if animated {
|
||||
taskHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak taskHighlightingNode] _ in
|
||||
taskHighlightingNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
taskHighlightingNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func animateTaskItemHighlightIn(id: Int32, sourceFrame: CGRect, transition: ContainedViewLayoutTransition) -> CGRect? {
|
||||
if let taskHighlightingNode = self.taskHighlightingNode {
|
||||
var currentRect = CGRect()
|
||||
for rect in taskHighlightingNode.rects {
|
||||
if currentRect.isEmpty {
|
||||
currentRect = rect
|
||||
} else {
|
||||
currentRect = currentRect.union(rect)
|
||||
}
|
||||
}
|
||||
if !currentRect.isEmpty {
|
||||
currentRect = currentRect.insetBy(dx: -taskHighlightingNode.inset, dy: -taskHighlightingNode.inset)
|
||||
let innerRect = currentRect.offsetBy(dx: taskHighlightingNode.frame.minX, dy: taskHighlightingNode.frame.minY)
|
||||
|
||||
taskHighlightingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1, delay: 0.04)
|
||||
|
||||
let fromScale = CGPoint(x: sourceFrame.width / innerRect.width, y: sourceFrame.height / innerRect.height)
|
||||
|
||||
var fromTransform = CATransform3DIdentity
|
||||
let fromOffset = CGPoint(x: sourceFrame.midX - innerRect.midX, y: sourceFrame.midY - innerRect.midY)
|
||||
|
||||
fromTransform = CATransform3DTranslate(fromTransform, fromOffset.x, fromOffset.y, 0.0)
|
||||
|
||||
fromTransform = CATransform3DTranslate(fromTransform, -taskHighlightingNode.bounds.width * 0.5 + currentRect.midX, -taskHighlightingNode.bounds.height * 0.5 + currentRect.midY, 0.0)
|
||||
fromTransform = CATransform3DScale(fromTransform, fromScale.x, fromScale.y, 1.0)
|
||||
fromTransform = CATransform3DTranslate(fromTransform, taskHighlightingNode.bounds.width * 0.5 - currentRect.midX, taskHighlightingNode.bounds.height * 0.5 - currentRect.midY, 0.0)
|
||||
|
||||
taskHighlightingNode.transform = fromTransform
|
||||
transition.updateTransform(node: taskHighlightingNode, transform: CGAffineTransformIdentity)
|
||||
|
||||
return currentRect.offsetBy(dx: taskHighlightingNode.frame.minX, dy: taskHighlightingNode.frame.minY)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -182,7 +182,7 @@ final class ComposeTodoScreenComponent: Component {
|
||||
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
||||
let localPoint = self.todoItemsSectionContainer.convert(point, from: self)
|
||||
for (id, itemView) in self.todoItemsSectionContainer.itemViews {
|
||||
if let view = itemView.contents.view {
|
||||
if let view = itemView.contents.view as? ListComposePollOptionComponent.View, !view.isRevealed {
|
||||
let viewFrame = view.convert(view.bounds, to: self.todoItemsSectionContainer)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: viewFrame.maxX - viewFrame.height, y: viewFrame.minY), size: CGSize(width: viewFrame.height, height: viewFrame.height))
|
||||
if iconFrame.contains(localPoint) {
|
||||
@ -914,6 +914,13 @@ final class ComposeTodoScreenComponent: Component {
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
},
|
||||
deleteAction: isEnabled ? { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.todoItems.removeAll(where: { $0.id == optionId })
|
||||
self.state?.updated(transition: .spring(duration: 0.4))
|
||||
} : nil,
|
||||
tag: todoItem.textFieldTag
|
||||
))))
|
||||
|
||||
@ -1692,212 +1699,3 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (AnyHashable) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGPoint) -> Void
|
||||
private let isActiveUpdated: (Bool) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: SwiftSignalKit.Timer?
|
||||
private var longPressTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var id: AnyHashable?
|
||||
private var itemView: ComponentView<Empty>?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
self.isActiveUpdated = isActiveUpdated
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
self?.longTapTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longTapTimer = longTapTimer
|
||||
longTapTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemView = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
||||
self?.longPressTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer = longPressTimer
|
||||
longPressTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemView = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemView = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
|
||||
self.isActiveUpdated(false)
|
||||
}
|
||||
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isActiveUpdated(true)
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let id = self.id {
|
||||
self.began(id)
|
||||
}
|
||||
self.isActiveUpdated(true)
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
self.ended()
|
||||
return
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.isActiveUpdated(true)
|
||||
|
||||
self.id = id
|
||||
self.itemView = itemView
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
if let id = self.id {
|
||||
self.began(id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.isActiveUpdated(false)
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
let dY = touchLocation.y - initialTapLocation.y
|
||||
|
||||
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath)
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32)
|
||||
}
|
||||
|
||||
@ -86,6 +86,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
public let inputMode: InputMode?
|
||||
public let alwaysDisplayInputModeSelector: Bool
|
||||
public let toggleInputMode: (() -> Void)?
|
||||
public let deleteAction: (() -> Void)?
|
||||
public let tag: AnyObject?
|
||||
|
||||
public init(
|
||||
@ -107,6 +108,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
inputMode: InputMode?,
|
||||
alwaysDisplayInputModeSelector: Bool = false,
|
||||
toggleInputMode: (() -> Void)?,
|
||||
deleteAction: (() -> Void)? = nil,
|
||||
tag: AnyObject? = nil
|
||||
) {
|
||||
self.externalState = externalState
|
||||
@ -127,6 +129,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
self.inputMode = inputMode
|
||||
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
|
||||
self.toggleInputMode = toggleInputMode
|
||||
self.deleteAction = deleteAction
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
@ -176,7 +179,9 @@ public final class ListComposePollOptionComponent: Component {
|
||||
if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector {
|
||||
return false
|
||||
}
|
||||
|
||||
if (lhs.deleteAction == nil) != (rhs.deleteAction == nil) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -253,7 +258,115 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView {
|
||||
private final class DeleteRevealView: UIView {
|
||||
private let backgroundView: UIView
|
||||
|
||||
private let _title: String
|
||||
private let title = ComponentView<Empty>()
|
||||
|
||||
private var revealOffset: CGFloat = 0.0
|
||||
|
||||
var currentSize = CGSize()
|
||||
|
||||
var tapped: (Bool) -> Void = { _ in }
|
||||
|
||||
init(title: String, color: UIColor) {
|
||||
self._title = title
|
||||
|
||||
self.backgroundView = UIView()
|
||||
self.backgroundView.backgroundColor = color
|
||||
self.backgroundView.isUserInteractionEnabled = false
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:)))
|
||||
self.addGestureRecognizer(tapRecognizer)
|
||||
}
|
||||
|
||||
required public init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecignizer: UITapGestureRecognizer) {
|
||||
let location = gestureRecignizer.location(in: self)
|
||||
if self.backgroundView.frame.contains(location) {
|
||||
self.tapped(true)
|
||||
} else {
|
||||
self.tapped(false)
|
||||
}
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if abs(self.revealOffset) < .ulpOfOne {
|
||||
return false
|
||||
}
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
|
||||
func updateLayout(availableSize: CGSize, revealOffset: CGFloat, transition: ComponentTransition) -> CGSize {
|
||||
let previousRevealOffset = self.revealOffset
|
||||
self.revealOffset = revealOffset
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(
|
||||
NSAttributedString(
|
||||
string: self._title,
|
||||
font: Font.regular(17.0),
|
||||
textColor: .white
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let size = CGSize(width: max(74.0, titleSize.width + 20.0), height: availableSize.height)
|
||||
let previousRevealFactor = abs(previousRevealOffset) / size.width
|
||||
let revealFactor = abs(revealOffset) / size.width
|
||||
let backgroundWidth = size.width * max(1.0, abs(revealFactor))
|
||||
|
||||
let previousIsExtended = previousRevealFactor >= 2.0
|
||||
let isExtended = revealFactor >= 2.0
|
||||
var titleTransition = transition
|
||||
if isExtended != previousIsExtended {
|
||||
titleTransition = .spring(duration: 0.3)
|
||||
}
|
||||
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.backgroundView.addSubview(titleView)
|
||||
}
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(
|
||||
x: revealFactor > 2.0 ? 10.0 : max(10.0, backgroundWidth - titleSize.width - 10.0),
|
||||
y: floor((size.height - titleSize.height) / 2.0)
|
||||
),
|
||||
size: titleSize
|
||||
)
|
||||
|
||||
if titleTransition.animation.isImmediate && titleView.layer.animation(forKey: "position") != nil {
|
||||
} else {
|
||||
titleTransition.setFrame(view: titleView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: availableSize.width + revealOffset, y: 0.0), size: CGSize(width: backgroundWidth, height: size.height))
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
|
||||
self.currentSize = size
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
public final class View: UIView, ListSectionComponent.ChildView, ComponentTaggedView, UIGestureRecognizerDelegate {
|
||||
private let textField = ComponentView<Empty>()
|
||||
|
||||
private var modeSelector: ComponentView<Empty>?
|
||||
@ -261,6 +374,12 @@ public final class ListComposePollOptionComponent: Component {
|
||||
|
||||
private var checkView: CheckView?
|
||||
|
||||
private var deleteRevealView: DeleteRevealView?
|
||||
private var revealOffset: CGFloat = 0.0
|
||||
public private(set) var isRevealed: Bool = false
|
||||
|
||||
private var recognizer: RevealOptionsGestureRecognizer?
|
||||
|
||||
private var customPlaceholder: ComponentView<Empty>?
|
||||
|
||||
private var component: ListComposePollOptionComponent?
|
||||
@ -293,7 +412,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
|
||||
public var customUpdateIsHighlighted: ((Bool) -> Void)?
|
||||
public private(set) var separatorInset: CGFloat = 0.0
|
||||
|
||||
|
||||
public override init(frame: CGRect) {
|
||||
super.init(frame: CGRect())
|
||||
}
|
||||
@ -330,6 +449,97 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
override public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let recognizer = self.recognizer, gestureRecognizer == self.recognizer, recognizer.numberOfTouches == 0 {
|
||||
let translation = recognizer.velocity(in: recognizer.view)
|
||||
if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if gestureRecognizer == self.recognizer, let externalState = self.component?.externalState, !externalState.hasText {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let component = self.component, component.deleteAction != nil else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = gestureRecognizer.translation(in: self)
|
||||
let velocity = gestureRecognizer.velocity(in: self)
|
||||
let revealWidth: CGFloat = self.deleteRevealView?.currentSize.width ?? 74.0
|
||||
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.window?.endEditing(true)
|
||||
|
||||
if self.isRevealed {
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
if location.x > self.bounds.width - revealWidth {
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
case .changed:
|
||||
var offset = self.revealOffset + translation.x
|
||||
offset = max(-revealWidth * 6.0, min(0, offset))
|
||||
|
||||
self.revealOffset = offset
|
||||
self.state?.updated()
|
||||
gestureRecognizer.setTranslation(CGPoint(), in: self)
|
||||
|
||||
case .ended, .cancelled:
|
||||
var shouldReveal = false
|
||||
|
||||
if abs(velocity.x) >= 100.0 {
|
||||
shouldReveal = velocity.x < 0
|
||||
} else {
|
||||
if self.revealOffset.isZero && self.revealOffset < 0 {
|
||||
shouldReveal = self.revealOffset < -revealWidth * 0.5
|
||||
} else if self.isRevealed {
|
||||
shouldReveal = self.revealOffset < -revealWidth * 0.3
|
||||
} else {
|
||||
shouldReveal = self.revealOffset < -revealWidth * 0.5
|
||||
}
|
||||
}
|
||||
|
||||
let isExtendedSwipe = self.revealOffset < -revealWidth * 2.0
|
||||
|
||||
if isExtendedSwipe && shouldReveal {
|
||||
component.deleteAction?()
|
||||
|
||||
self.isRevealed = false
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
self.revealOffset = 0.0
|
||||
self.state?.updated(transition: transition)
|
||||
} else {
|
||||
let targetOffset: CGFloat = shouldReveal ? -revealWidth : 0.0
|
||||
self.isRevealed = shouldReveal
|
||||
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
self.revealOffset = targetOffset
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard self.isRevealed else {
|
||||
return
|
||||
}
|
||||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
if location.x >= self.bounds.width + self.revealOffset {
|
||||
self.component?.deleteAction?()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.isUpdating = true
|
||||
defer {
|
||||
@ -405,7 +615,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
)
|
||||
|
||||
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)
|
||||
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0 + self.revealOffset, y: 0.0), size: textFieldSize)
|
||||
|
||||
if let textFieldView = self.textField.view {
|
||||
if textFieldView.superview == nil {
|
||||
@ -437,7 +647,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5), y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
|
||||
let checkFrame = CGRect(origin: CGPoint(x: floor((leftInset - checkSize.width) * 0.5) + self.revealOffset, y: floor((size.height - checkSize.height) * 0.5)), size: checkSize)
|
||||
|
||||
if animateIn {
|
||||
checkView.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)
|
||||
@ -475,7 +685,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
reorderIconSize = icon.size
|
||||
}
|
||||
|
||||
let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize)
|
||||
let reorderIconFrame = CGRect(origin: CGPoint(x: size.width - 14.0 - reorderIconSize.width + self.revealOffset, y: floor((size.height - reorderIconSize.height) * 0.5)), size: reorderIconSize)
|
||||
reorderIconTransition.setPosition(view: reorderIconView, position: reorderIconFrame.center)
|
||||
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
|
||||
|
||||
@ -539,7 +749,7 @@ public final class ListComposePollOptionComponent: Component {
|
||||
environment: {},
|
||||
containerSize: modeSelectorSize
|
||||
)
|
||||
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||
let modeSelectorFrame = CGRect(origin: CGPoint(x: size.width - rightIconsInset - 4.0 - modeSelectorSize.width + self.revealOffset, y: floor((size.height - modeSelectorSize.height) * 0.5)), size: modeSelectorSize)
|
||||
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||
|
||||
@ -580,6 +790,51 @@ public final class ListComposePollOptionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let deleteAction = component.deleteAction {
|
||||
if self.deleteRevealView == nil {
|
||||
let deleteRevealView = DeleteRevealView(title: component.strings.Common_Delete, color: component.theme.list.itemDisclosureActions.destructive.fillColor)
|
||||
deleteRevealView.tapped = { [weak self] action in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if action {
|
||||
deleteAction()
|
||||
} else {
|
||||
self.revealOffset = 0.0
|
||||
self.isRevealed = false
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
}
|
||||
self.deleteRevealView = deleteRevealView
|
||||
self.addSubview(deleteRevealView)
|
||||
}
|
||||
|
||||
if self.recognizer == nil {
|
||||
let recognizer = RevealOptionsGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
recognizer.delegate = self
|
||||
self.addGestureRecognizer(recognizer)
|
||||
self.recognizer = recognizer
|
||||
}
|
||||
} else {
|
||||
if let deleteRevealView = self.deleteRevealView {
|
||||
self.deleteRevealView = nil
|
||||
deleteRevealView.removeFromSuperview()
|
||||
}
|
||||
|
||||
if let panGestureRecognizer = self.recognizer {
|
||||
self.recognizer = nil
|
||||
self.removeGestureRecognizer(panGestureRecognizer)
|
||||
}
|
||||
|
||||
self.isRevealed = false
|
||||
self.revealOffset = 0.0
|
||||
}
|
||||
|
||||
if let deleteRevealView = self.deleteRevealView {
|
||||
let _ = deleteRevealView.updateLayout(availableSize: size, revealOffset: self.revealOffset, transition: transition)
|
||||
deleteRevealView.frame = CGRect(origin: .zero, size: size)
|
||||
}
|
||||
|
||||
self.separatorInset = leftInset
|
||||
|
||||
return size
|
||||
@ -646,3 +901,58 @@ public final class ListComposePollOptionComponent: Component {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class RevealOptionsGestureRecognizer: UIPanGestureRecognizer {
|
||||
var validatedGesture = false
|
||||
var firstLocation: CGPoint = CGPoint()
|
||||
|
||||
var allowAnyDirection = false
|
||||
var lastVelocity: CGPoint = CGPoint()
|
||||
|
||||
override public init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
if #available(iOS 13.4, *) {
|
||||
self.allowedScrollTypesMask = .continuous
|
||||
}
|
||||
|
||||
self.maximumNumberOfTouches = 1
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.validatedGesture = false
|
||||
}
|
||||
|
||||
func becomeCancelled() {
|
||||
self.state = .cancelled
|
||||
}
|
||||
|
||||
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
let touch = touches.first!
|
||||
self.firstLocation = touch.location(in: self.view)
|
||||
}
|
||||
|
||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
let location = touches.first!.location(in: self.view)
|
||||
let translation = CGPoint(x: location.x - self.firstLocation.x, y: location.y - self.firstLocation.y)
|
||||
|
||||
if !self.validatedGesture {
|
||||
if !self.allowAnyDirection && translation.x > 0.0 {
|
||||
self.state = .failed
|
||||
} else if abs(translation.y) > 4.0 && abs(translation.y) > abs(translation.x) * 2.5 {
|
||||
self.state = .failed
|
||||
} else if abs(translation.x) > 4.0 && abs(translation.y) * 2.5 < abs(translation.x) {
|
||||
self.validatedGesture = true
|
||||
}
|
||||
}
|
||||
|
||||
if self.validatedGesture {
|
||||
self.lastVelocity = self.velocity(in: self.view)
|
||||
super.touchesMoved(touches, with: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,214 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
|
||||
public final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (AnyHashable) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGPoint) -> Void
|
||||
private let isActiveUpdated: (Bool) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: SwiftSignalKit.Timer?
|
||||
private var longPressTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var id: AnyHashable?
|
||||
private var itemView: ComponentView<Empty>?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, id: AnyHashable?, item: ComponentView<Empty>?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (AnyHashable) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
self.isActiveUpdated = isActiveUpdated
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
self?.longTapTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longTapTimer = longTapTimer
|
||||
longTapTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemView = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
||||
self?.longPressTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer = longPressTimer
|
||||
longPressTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemView = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemView = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
|
||||
self.isActiveUpdated(false)
|
||||
}
|
||||
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isActiveUpdated(true)
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let id = self.id {
|
||||
self.began(id)
|
||||
}
|
||||
self.isActiveUpdated(true)
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
self.ended()
|
||||
return
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, id, itemView) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.isActiveUpdated(true)
|
||||
|
||||
self.id = id
|
||||
self.itemView = itemView
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
if let id = self.id {
|
||||
self.began(id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.isActiveUpdated(false)
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
let dY = touchLocation.y - initialTapLocation.y
|
||||
|
||||
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func generateReorderingBackgroundImage(backgroundColor: UIColor) -> UIImage? {
|
||||
return generateImage(CGSize(width: 64.0, height: 64.0), contextGenerator: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.addPath(UIBezierPath(roundedRect: CGRect(x: 10, y: 10, width: 44, height: 44), cornerRadius: 10).cgPath)
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 24.0, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 32, topCapHeight: 32)
|
||||
}
|
||||
@ -368,7 +368,7 @@ final class PeerInfoListPaneNode: ASDisplayNode, PeerInfoPaneNode {
|
||||
}
|
||||
if let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation {
|
||||
if type == .music {
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId), quote: nil), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
|
||||
let signal = strongSelf.context.sharedContext.messageFromPreloadedChatHistoryViewForLocation(id: id.messageId, location: ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(id.messageId)), count: 60, highlight: true, setupReply: false), id: 0), context: strongSelf.context, chatLocation: .peer(id: id.messageId.peerId), subject: nil, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), tag: .tag(MessageTags.music))
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
@ -110,7 +110,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -204,7 +204,7 @@ extension ChatControllerImpl {
|
||||
|
||||
let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil, setupReply: false)
|
||||
|
||||
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: false), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation), count: 50, highlight: true, setupReply: false), id: 0), context: self.context, chatLocation: preloadChatLocation, subject: subject, chatLocationContextHolder: Atomic<ChatLocationContextHolder?>(value: nil), fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
|
||||
var signal: Signal<(MessageIndex?, Bool), NoError>
|
||||
signal = historyView
|
||||
@ -399,7 +399,7 @@ extension ChatControllerImpl {
|
||||
}
|
||||
}
|
||||
var historyView: Signal<ChatHistoryViewUpdate, NoError>
|
||||
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: nil), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
|
||||
var signal: Signal<(MessageIndex?, Bool), NoError>
|
||||
signal = historyView
|
||||
@ -512,13 +512,15 @@ extension ChatControllerImpl {
|
||||
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
||||
|
||||
var quote: ChatControllerSubject.MessageHighlight.Quote?
|
||||
var todoTaskId: Int32?
|
||||
var setupReply = false
|
||||
if case let .id(_, params) = messageLocation {
|
||||
quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) }
|
||||
todoTaskId = params.todoTaskId
|
||||
setupReply = params.setupReply
|
||||
}
|
||||
|
||||
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
let historyView = preloadedChatHistoryViewForLocation(ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: searchLocation, quote: quote.flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: todoTaskId), count: 50, highlight: true, setupReply: setupReply), id: 0), context: self.context, chatLocation: self.chatLocation, subject: self.subject, chatLocationContextHolder: self.chatLocationContextHolder, fixedCombinedReadStates: nil, tag: nil, additionalData: [])
|
||||
var signal: Signal<MessageIndex?, NoError>
|
||||
signal = historyView
|
||||
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
|
||||
|
||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditorScreen
|
||||
import WallpaperGalleryScreen
|
||||
|
||||
@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode
|
||||
import ChatMessageBubbleItemNode
|
||||
import ChatNavigationButton
|
||||
import WebsiteType
|
||||
import ChatQrCodeScreen
|
||||
import PeerInfoScreen
|
||||
import MediaEditor
|
||||
import MediaEditorScreen
|
||||
@ -6155,7 +6154,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
|
||||
let location: ChatHistoryLocation
|
||||
if let anchorMessageId = anchorMessageId {
|
||||
location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId), quote: nil), count: count, highlight: false, setupReply: false)
|
||||
location = .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(anchorMessageId)), count: count, highlight: false, setupReply: false)
|
||||
} else {
|
||||
location = .Initial(count: count)
|
||||
}
|
||||
@ -8592,9 +8591,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
case let .chat(textInputState, subject, peekData):
|
||||
dismissWebAppControllers()
|
||||
if case .peer(peerId.id) = strongSelf.chatLocation {
|
||||
if let subject = subject, case let .message(messageSubject, _, timecode, _) = subject {
|
||||
if let subject = subject, case let .message(messageSubject, highlight, timecode, _) = subject {
|
||||
if case let .id(messageId) = messageSubject {
|
||||
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil)))
|
||||
strongSelf.navigateToMessage(from: sourceMessageId, to: .id(messageId, NavigateToMessageParams(timestamp: timecode, quote: nil, todoTaskId: highlight?.todoTaskId)))
|
||||
}
|
||||
} else {
|
||||
self?.playShakeAnimation()
|
||||
|
||||
@ -10,7 +10,6 @@ import Display
|
||||
import UIKit
|
||||
import UndoUI
|
||||
import ShareController
|
||||
import ChatQrCodeScreen
|
||||
import ChatShareMessageTagView
|
||||
import ReactionSelectionNode
|
||||
import TopMessageReactions
|
||||
@ -114,7 +113,7 @@ extension ChatControllerImpl {
|
||||
shareController.parentNavigationController = self.navigationController as? NavigationController
|
||||
|
||||
if let message = messages.first, message.media.contains(where: { media in
|
||||
if media is TelegramMediaContact || media is TelegramMediaPoll {
|
||||
if media is TelegramMediaContact || media is TelegramMediaPoll || media is TelegramMediaTodo {
|
||||
return true
|
||||
} else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
||||
return true
|
||||
@ -139,12 +138,6 @@ extension ChatControllerImpl {
|
||||
}
|
||||
}
|
||||
}
|
||||
shareController.openShareAsImage = { [weak self] messages in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.present(ChatQrCodeScreenImpl(context: self.context, subject: .messages(messages)), in: .window(.root))
|
||||
}
|
||||
shareController.dismissed = { [weak self] shared in
|
||||
if shared {
|
||||
self?.commitPurposefulAction()
|
||||
|
||||
@ -993,9 +993,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
initialSearchLocation = .index(MessageIndex.absoluteUpperBound())
|
||||
}
|
||||
}
|
||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0)
|
||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: 0)
|
||||
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
|
||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: 0)
|
||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: 0)
|
||||
} else {
|
||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0)
|
||||
}
|
||||
@ -1815,9 +1815,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
||||
initialSearchLocation = .index(.absoluteUpperBound())
|
||||
}
|
||||
}
|
||||
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
||||
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: initialSearchLocation, quote: (highlight?.quote).flatMap { quote in MessageHistoryInitialSearchSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: highlight?.todoTaskId), count: historyMessageCount, highlight: highlight != nil, setupReply: setupReply), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
||||
} else if let subject = subject, case let .pinnedMessages(maybeMessageId) = subject, let messageId = maybeMessageId {
|
||||
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId), quote: nil), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
||||
strongSelf.chatHistoryLocationValue = ChatHistoryLocationInput(content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(messageId)), count: historyMessageCount, highlight: true, setupReply: false), id: (strongSelf.chatHistoryLocationValue?.id).flatMap({ $0 + 1 }) ?? 0)
|
||||
} else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue {
|
||||
chatHistoryLocation.id += 1
|
||||
strongSelf.chatHistoryLocationValue = chatHistoryLocation
|
||||
|
||||
@ -282,7 +282,7 @@ func chatHistoryViewForLocation(
|
||||
|
||||
preloaded = true
|
||||
|
||||
return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
|
||||
return .HistoryView(view: view, type: reportUpdateType, scrollPosition: .index(subject: MessageHistoryScrollToSubject(index: anchorIndex, quote: searchLocationSubject.quote.flatMap { quote in MessageHistoryScrollToSubject.Quote(string: quote.string, offset: quote.offset) }, todoTaskId: searchLocationSubject.todoTaskId, setupReply: setupReply), position: .center(.bottom), directionHint: .Down, animated: false, highlight: highlight, displayLink: false, setupReply: setupReply), flashIndicators: false, originalScrollPosition: nil, initialData: ChatHistoryCombinedInitialData(initialData: initialData, buttonKeyboardMessage: view.topTaggedMessages.first, cachedData: cachedData, cachedDataMessages: cachedDataMessages, readStateData: readStateData), id: location.id)
|
||||
}
|
||||
}
|
||||
case let .Navigation(index, anchorIndex, count, _):
|
||||
@ -415,7 +415,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
||||
case .automatic:
|
||||
if let atMessageId = atMessageId {
|
||||
input = ChatHistoryLocationInput(
|
||||
content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId), quote: nil), count: 40, highlight: true, setupReply: false),
|
||||
content: .InitialSearch(subject: MessageHistoryInitialSearchSubject(location: .id(atMessageId)), count: 40, highlight: true, setupReply: false),
|
||||
id: 0
|
||||
)
|
||||
} else {
|
||||
|
||||
@ -87,7 +87,7 @@ public enum ParsedInternalUrl {
|
||||
|
||||
case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?)
|
||||
case peerId(PeerId)
|
||||
case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?)
|
||||
case privateMessage(messageId: MessageId, threadId: Int32?, timecode: Double?, taskId: Int32?)
|
||||
case stickerPack(name: String, type: StickerPackUrlType)
|
||||
case invoice(String)
|
||||
case join(String)
|
||||
@ -545,6 +545,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
||||
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]), channelId > 0 {
|
||||
var threadId: Int32?
|
||||
var timecode: Double?
|
||||
var taskId: Int32?
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
@ -557,11 +558,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
||||
if timestampValue != 0 {
|
||||
timecode = Double(timestampValue)
|
||||
}
|
||||
} else if queryItem.name == "task" {
|
||||
if let intValue = Int32(value) {
|
||||
taskId = intValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode)
|
||||
return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -574,6 +579,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
||||
} else if pathComponents.count == 4 && pathComponents[0] == "c" {
|
||||
if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]), channelId > 0 {
|
||||
var timecode: Double?
|
||||
var taskId: Int32?
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
@ -582,11 +588,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
||||
if timestampValue != 0 {
|
||||
timecode = Double(timestampValue)
|
||||
}
|
||||
} else if queryItem.name == "task" {
|
||||
if let intValue = Int32(value) {
|
||||
taskId = intValue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode)
|
||||
return .privateMessage(messageId: MessageId(peerId: PeerId(namespace: Namespaces.Peer.CloudChannel, id: PeerId.Id._internalFromInt64Value(channelId)), namespace: Namespaces.Message.Cloud, id: messageId), threadId: threadId, timecode: timecode, taskId: taskId)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -935,7 +945,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
||||
return .single(.result(.peer(nil, .info(nil))))
|
||||
}
|
||||
})
|
||||
case let .privateMessage(messageId, threadId, timecode):
|
||||
case let .privateMessage(messageId, threadId, timecode, taskId):
|
||||
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|
||||
|> mapToSignal { peer -> Signal<ResolveInternalUrlResult, NoError> in
|
||||
let foundPeer: Signal<EnginePeer?, NoError>
|
||||
@ -1018,7 +1028,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
||||
return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId))
|
||||
})
|
||||
} else {
|
||||
return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil), timecode: timecode, setupReply: false), peekData: nil))))
|
||||
return .single(.result(.peer(foundPeer._asPeer(), .chat(textInputState: nil, subject: .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: nil, todoTaskId: taskId), timecode: timecode, setupReply: false), peekData: nil))))
|
||||
}
|
||||
} else {
|
||||
return .single(.result(.inaccessiblePeer))
|
||||
|
||||
@ -1753,6 +1753,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string)
|
||||
})
|
||||
}
|
||||
case "web_app_hide_keyboard":
|
||||
self.view.window?.endEditing(true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user