mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +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.TodoCompleted" = "%1$@ marked \"%2$@\" as done.";
|
||||||
"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone.";
|
"Notification.TodoIncompleted" = "%1$@ marked \"%2$@\" as undone.";
|
||||||
"Notification.TodoAddedTask" = "%1$@ added a new task \"%2$@\" to \"%3$@\".";
|
"Notification.TodoMultipleCompleted" = "%1$@ marked %2$@ as done.";
|
||||||
"Notification.TodoAddedMultipleTasks" = "%1$@ added %2$@ to \"%3$@\".";
|
"Notification.TodoMultipleIncompleted" = "%1$@ marked %2$@ as undone.";
|
||||||
|
|
||||||
"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done.";
|
"Notification.TodoCompletedYou" = "You marked \"%1$@\" as done.";
|
||||||
"Notification.TodoIncompletedYou" = "You marked \"%1$@\" as not 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.TodoAddedTaskYou" = "You added a new task \"%1$@\" to \"%2$@\".";
|
||||||
"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\".";
|
"Notification.TodoAddedMultipleTasksYou" = "You added %1$@ to \"%2$@\".";
|
||||||
|
|
||||||
|
|||||||
@ -778,9 +778,11 @@ public enum ChatControllerSubject: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var quote: Quote?
|
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.quote = quote
|
||||||
|
self.todoTaskId = todoTaskId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ public struct MessageHistoryScrollToSubject: Equatable {
|
|||||||
public var todoTaskId: Int32?
|
public var todoTaskId: Int32?
|
||||||
public var setupReply: Bool
|
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.index = index
|
||||||
self.quote = quote
|
self.quote = quote
|
||||||
self.todoTaskId = todoTaskId
|
self.todoTaskId = todoTaskId
|
||||||
@ -44,10 +44,12 @@ public struct MessageHistoryInitialSearchSubject: Equatable {
|
|||||||
|
|
||||||
public var location: ChatHistoryInitialSearchLocation
|
public var location: ChatHistoryInitialSearchLocation
|
||||||
public var quote: Quote?
|
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.location = location
|
||||||
self.quote = quote
|
self.quote = quote
|
||||||
|
self.todoTaskId = todoTaskId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4755,7 +4755,7 @@ final class ChatListSearchListPaneNode: ASDisplayNode, ChatListSearchPaneNode {
|
|||||||
strongSelf.interaction.dismissInput()
|
strongSelf.interaction.dismissInput()
|
||||||
strongSelf.interaction.present(controller, nil)
|
strongSelf.interaction.present(controller, nil)
|
||||||
} else if case let .messages(chatLocation, _, _) = playlistLocation {
|
} 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)?
|
var cancelImpl: (() -> Void)?
|
||||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|||||||
@ -165,6 +165,9 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
private var currentEditingTag: AnyObject?
|
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) {
|
override init(frame: CGRect) {
|
||||||
self.scrollView = UIScrollView()
|
self.scrollView = UIScrollView()
|
||||||
self.scrollView.showsVerticalScrollIndicator = true
|
self.scrollView.showsVerticalScrollIndicator = true
|
||||||
@ -181,6 +184,39 @@ final class ComposePollScreenComponent: Component {
|
|||||||
|
|
||||||
self.scrollView.delegate = self
|
self.scrollView.delegate = self
|
||||||
self.addSubview(self.scrollView)
|
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) {
|
required init?(coder: NSCoder) {
|
||||||
@ -195,6 +231,114 @@ final class ComposePollScreenComponent: Component {
|
|||||||
self.scrollView.setContentOffset(CGPoint(), animated: true)
|
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? {
|
func validatedInput() -> ComposedPoll? {
|
||||||
if self.pollTextInputState.text.length == 0 {
|
if self.pollTextInputState.text.length == 0 {
|
||||||
return nil
|
return nil
|
||||||
@ -799,6 +943,7 @@ final class ComposePollScreenComponent: Component {
|
|||||||
},
|
},
|
||||||
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
|
assumeIsEditing: self.inputMediaNodeTargetTag === pollOption.textFieldTag,
|
||||||
characterLimit: component.initialData.maxPollOptionLength,
|
characterLimit: component.initialData.maxPollOptionLength,
|
||||||
|
canReorder: true,
|
||||||
emptyLineHandling: .notAllowed,
|
emptyLineHandling: .notAllowed,
|
||||||
returnKeyAction: { [weak self] in
|
returnKeyAction: { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -848,6 +993,13 @@ final class ComposePollScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
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
|
tag: pollOption.textFieldTag
|
||||||
))))
|
))))
|
||||||
|
|
||||||
@ -878,6 +1030,12 @@ final class ComposePollScreenComponent: Component {
|
|||||||
size: itemSize,
|
size: itemSize,
|
||||||
transition: itemTransition
|
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 {
|
for i in 0 ..< self.pollOptions.count {
|
||||||
|
|||||||
@ -864,7 +864,7 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
|||||||
strongSelf.displayNode.view.window?.endEditing(true)
|
strongSelf.displayNode.view.window?.endEditing(true)
|
||||||
strongSelf.present(controller, in: .window(.root))
|
strongSelf.present(controller, in: .window(.root))
|
||||||
} else if case let .messages(chatLocation, _, _) = playlistLocation {
|
} 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)?
|
var cancelImpl: (() -> Void)?
|
||||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
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 todo {
|
||||||
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
|
if let completedTaskId = completed.first, let completedTask = todo.items.first(where: { $0.id == completedTaskId }) {
|
||||||
taskTitle = completedTask.text
|
taskTitle = completedTask.text
|
||||||
@ -1306,16 +1306,20 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
taskTitle = incompletedTask.text
|
taskTitle = incompletedTask.text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if taskTitle.count > 20 {
|
if let taskTitleValue = taskTitle, taskTitleValue.count > 20 {
|
||||||
taskTitle = taskTitle.prefix(20) + "…"
|
taskTitle = taskTitleValue.prefix(20) + "…"
|
||||||
}
|
}
|
||||||
|
|
||||||
if message.author?.id == accountPeerId {
|
if message.author?.id == accountPeerId {
|
||||||
let resultString: PresentationStrings.FormattedString
|
let resultString: PresentationStrings.FormattedString
|
||||||
if let _ = completed.first {
|
if completed.count > 1 || (completed.count == 1 && taskTitle == nil) {
|
||||||
resultString = strings.Notification_TodoCompletedYou(taskTitle)
|
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 {
|
} else if let _ = incompleted.first {
|
||||||
resultString = strings.Notification_TodoIncompletedYou(taskTitle)
|
resultString = strings.Notification_TodoIncompletedYou(taskTitle ?? "")
|
||||||
} else {
|
} else {
|
||||||
resultString = strings.Notification_TodoCompletedYou("")
|
resultString = strings.Notification_TodoCompletedYou("")
|
||||||
}
|
}
|
||||||
@ -1327,10 +1331,14 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
attributes[1] = boldAttributes
|
attributes[1] = boldAttributes
|
||||||
|
|
||||||
let resultString: PresentationStrings.FormattedString
|
let resultString: PresentationStrings.FormattedString
|
||||||
if let _ = completed.first {
|
if completed.count > 1 {
|
||||||
resultString = strings.Notification_TodoCompleted(peerName, taskTitle)
|
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 {
|
} else if let _ = incompleted.first {
|
||||||
resultString = strings.Notification_TodoIncompleted(peerName, taskTitle)
|
resultString = strings.Notification_TodoIncompleted(peerName, taskTitle ?? "")
|
||||||
} else {
|
} else {
|
||||||
resultString = strings.Notification_TodoCompleted(peerName, "")
|
resultString = strings.Notification_TodoCompleted(peerName, "")
|
||||||
}
|
}
|
||||||
@ -1359,7 +1367,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
}
|
}
|
||||||
resultString = strings.Notification_TodoAddedTaskYou(taskTitle, todoTitle)
|
resultString = strings.Notification_TodoAddedTaskYou(taskTitle, todoTitle)
|
||||||
} else {
|
} 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])
|
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: [0: boldAttributes, 1: boldAttributes])
|
||||||
} else {
|
} else {
|
||||||
@ -1376,7 +1384,7 @@ public func universalServiceMessageString(presentationData: (PresentationTheme,
|
|||||||
}
|
}
|
||||||
resultString = strings.Notification_TodoAddedTask(peerName, taskTitle, todoTitle)
|
resultString = strings.Notification_TodoAddedTask(peerName, taskTitle, todoTitle)
|
||||||
} else {
|
} 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)
|
attributedString = addAttributesToStringWithRanges(resultString._tuple, body: bodyAttributes, argumentAttributes: attributes)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5916,8 +5916,8 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
|
|||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
|
if let contentNode = contentNode as? ChatMessageTextBubbleContentNode {
|
||||||
contentNode.updateQuoteTextHighlightState(text: nil, offset: nil, color: .clear, animated: true)
|
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?
|
var taskFrame: CGRect?
|
||||||
for contentNode in self.contentNodes {
|
for contentNode in self.contentNodes {
|
||||||
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode, let localFrame = contentNode.taskItemFrame(id: todoTaskId) {
|
if let contentNode = contentNode as? ChatMessageTodoBubbleContentNode {
|
||||||
taskFrame = contentNode.view.convert(localFrame, to: backgroundHighlightNode.view.superview)
|
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
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1242,10 +1242,73 @@ public class ChatMessageTodoBubbleContentNode: ChatMessageBubbleContentNode {
|
|||||||
return nil
|
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 {
|
for node in self.optionNodes {
|
||||||
if node.option?.id == id {
|
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
|
return nil
|
||||||
|
|||||||
@ -182,7 +182,7 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
private func item(at point: CGPoint) -> (AnyHashable, ComponentView<Empty>)? {
|
||||||
let localPoint = self.todoItemsSectionContainer.convert(point, from: self)
|
let localPoint = self.todoItemsSectionContainer.convert(point, from: self)
|
||||||
for (id, itemView) in self.todoItemsSectionContainer.itemViews {
|
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 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))
|
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) {
|
if iconFrame.contains(localPoint) {
|
||||||
@ -914,6 +914,13 @@ final class ComposeTodoScreenComponent: Component {
|
|||||||
}
|
}
|
||||||
self.state?.updated(transition: .spring(duration: 0.4))
|
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
|
tag: todoItem.textFieldTag
|
||||||
))))
|
))))
|
||||||
|
|
||||||
@ -1692,212 +1699,3 @@ public class ComposeTodoScreen: ViewControllerComponentContainer, AttachmentCont
|
|||||||
return true
|
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 inputMode: InputMode?
|
||||||
public let alwaysDisplayInputModeSelector: Bool
|
public let alwaysDisplayInputModeSelector: Bool
|
||||||
public let toggleInputMode: (() -> Void)?
|
public let toggleInputMode: (() -> Void)?
|
||||||
|
public let deleteAction: (() -> Void)?
|
||||||
public let tag: AnyObject?
|
public let tag: AnyObject?
|
||||||
|
|
||||||
public init(
|
public init(
|
||||||
@ -107,6 +108,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
inputMode: InputMode?,
|
inputMode: InputMode?,
|
||||||
alwaysDisplayInputModeSelector: Bool = false,
|
alwaysDisplayInputModeSelector: Bool = false,
|
||||||
toggleInputMode: (() -> Void)?,
|
toggleInputMode: (() -> Void)?,
|
||||||
|
deleteAction: (() -> Void)? = nil,
|
||||||
tag: AnyObject? = nil
|
tag: AnyObject? = nil
|
||||||
) {
|
) {
|
||||||
self.externalState = externalState
|
self.externalState = externalState
|
||||||
@ -127,6 +129,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
self.inputMode = inputMode
|
self.inputMode = inputMode
|
||||||
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
|
self.alwaysDisplayInputModeSelector = alwaysDisplayInputModeSelector
|
||||||
self.toggleInputMode = toggleInputMode
|
self.toggleInputMode = toggleInputMode
|
||||||
|
self.deleteAction = deleteAction
|
||||||
self.tag = tag
|
self.tag = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,7 +179,9 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector {
|
if lhs.alwaysDisplayInputModeSelector != rhs.alwaysDisplayInputModeSelector {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if (lhs.deleteAction == nil) != (rhs.deleteAction == nil) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
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 let textField = ComponentView<Empty>()
|
||||||
|
|
||||||
private var modeSelector: ComponentView<Empty>?
|
private var modeSelector: ComponentView<Empty>?
|
||||||
@ -261,6 +374,12 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
|
|
||||||
private var checkView: CheckView?
|
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 customPlaceholder: ComponentView<Empty>?
|
||||||
|
|
||||||
private var component: ListComposePollOptionComponent?
|
private var component: ListComposePollOptionComponent?
|
||||||
@ -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 {
|
func update(component: ListComposePollOptionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||||
self.isUpdating = true
|
self.isUpdating = true
|
||||||
defer {
|
defer {
|
||||||
@ -405,7 +615,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
)
|
)
|
||||||
|
|
||||||
let size = CGSize(width: availableSize.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)
|
let textFieldFrame = CGRect(origin: CGPoint(x: leftInset - 16.0 + self.revealOffset, y: 0.0), size: textFieldSize)
|
||||||
|
|
||||||
if let textFieldView = self.textField.view {
|
if let textFieldView = self.textField.view {
|
||||||
if textFieldView.superview == nil {
|
if textFieldView.superview == nil {
|
||||||
@ -437,7 +647,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let checkSize = CGSize(width: 22.0, height: 22.0)
|
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 {
|
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)
|
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
|
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.setPosition(view: reorderIconView, position: reorderIconFrame.center)
|
||||||
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
|
reorderIconTransition.setBounds(view: reorderIconView, bounds: CGRect(origin: CGPoint(), size: reorderIconFrame.size))
|
||||||
|
|
||||||
@ -539,7 +749,7 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: modeSelectorSize
|
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 {
|
if let modeSelectorView = modeSelector.view as? PlainButtonComponent.View {
|
||||||
let alphaTransition: ComponentTransition = .easeInOut(duration: 0.2)
|
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
|
self.separatorInset = leftInset
|
||||||
|
|
||||||
return size
|
return size
|
||||||
@ -646,3 +901,58 @@ public final class ListComposePollOptionComponent: Component {
|
|||||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
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 let id = state.id as? PeerMessagesMediaPlaylistItemId, let playlistLocation = strongSelf.playlistLocation as? PeerMessagesPlaylistLocation, case let .messages(chatLocation, _, _) = playlistLocation {
|
||||||
if type == .music {
|
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)?
|
var cancelImpl: (() -> Void)?
|
||||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||||
|
|||||||
@ -110,7 +110,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -204,7 +204,7 @@ extension ChatControllerImpl {
|
|||||||
|
|
||||||
let subject: ChatControllerSubject = .message(id: .id(messageId), highlight: ChatControllerSubject.MessageHighlight(quote: quote), timecode: nil, setupReply: false)
|
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>
|
var signal: Signal<(MessageIndex?, Bool), NoError>
|
||||||
signal = historyView
|
signal = historyView
|
||||||
@ -399,7 +399,7 @@ extension ChatControllerImpl {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var historyView: Signal<ChatHistoryViewUpdate, NoError>
|
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>
|
var signal: Signal<(MessageIndex?, Bool), NoError>
|
||||||
signal = historyView
|
signal = historyView
|
||||||
@ -512,13 +512,15 @@ extension ChatControllerImpl {
|
|||||||
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
self.loadingMessage.set(.single(statusSubject) |> delay(0.1, queue: .mainQueue()))
|
||||||
|
|
||||||
var quote: ChatControllerSubject.MessageHighlight.Quote?
|
var quote: ChatControllerSubject.MessageHighlight.Quote?
|
||||||
|
var todoTaskId: Int32?
|
||||||
var setupReply = false
|
var setupReply = false
|
||||||
if case let .id(_, params) = messageLocation {
|
if case let .id(_, params) = messageLocation {
|
||||||
quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) }
|
quote = params.quote.flatMap { quote in ChatControllerSubject.MessageHighlight.Quote(string: quote.string, offset: quote.offset) }
|
||||||
|
todoTaskId = params.todoTaskId
|
||||||
setupReply = params.setupReply
|
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>
|
var signal: Signal<MessageIndex?, NoError>
|
||||||
signal = historyView
|
signal = historyView
|
||||||
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
|
|> mapToSignal { historyView -> Signal<MessageIndex?, NoError> in
|
||||||
|
|||||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -111,7 +111,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
import WallpaperGalleryScreen
|
import WallpaperGalleryScreen
|
||||||
|
|||||||
@ -112,7 +112,6 @@ import ChatMessageAnimatedStickerItemNode
|
|||||||
import ChatMessageBubbleItemNode
|
import ChatMessageBubbleItemNode
|
||||||
import ChatNavigationButton
|
import ChatNavigationButton
|
||||||
import WebsiteType
|
import WebsiteType
|
||||||
import ChatQrCodeScreen
|
|
||||||
import PeerInfoScreen
|
import PeerInfoScreen
|
||||||
import MediaEditor
|
import MediaEditor
|
||||||
import MediaEditorScreen
|
import MediaEditorScreen
|
||||||
@ -6155,7 +6154,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
|
func pinnedHistorySignal(anchorMessageId: MessageId?, count: Int) -> Signal<ChatHistoryViewUpdate, NoError> {
|
||||||
let location: ChatHistoryLocation
|
let location: ChatHistoryLocation
|
||||||
if let anchorMessageId = anchorMessageId {
|
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 {
|
} else {
|
||||||
location = .Initial(count: count)
|
location = .Initial(count: count)
|
||||||
}
|
}
|
||||||
@ -8592,9 +8591,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
|||||||
case let .chat(textInputState, subject, peekData):
|
case let .chat(textInputState, subject, peekData):
|
||||||
dismissWebAppControllers()
|
dismissWebAppControllers()
|
||||||
if case .peer(peerId.id) = strongSelf.chatLocation {
|
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 {
|
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 {
|
} else {
|
||||||
self?.playShakeAnimation()
|
self?.playShakeAnimation()
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import Display
|
|||||||
import UIKit
|
import UIKit
|
||||||
import UndoUI
|
import UndoUI
|
||||||
import ShareController
|
import ShareController
|
||||||
import ChatQrCodeScreen
|
|
||||||
import ChatShareMessageTagView
|
import ChatShareMessageTagView
|
||||||
import ReactionSelectionNode
|
import ReactionSelectionNode
|
||||||
import TopMessageReactions
|
import TopMessageReactions
|
||||||
@ -114,7 +113,7 @@ extension ChatControllerImpl {
|
|||||||
shareController.parentNavigationController = self.navigationController as? NavigationController
|
shareController.parentNavigationController = self.navigationController as? NavigationController
|
||||||
|
|
||||||
if let message = messages.first, message.media.contains(where: { media in
|
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
|
return true
|
||||||
} else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
} else if let file = media as? TelegramMediaFile, file.isSticker || file.isAnimatedSticker || file.isVideoSticker {
|
||||||
return true
|
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
|
shareController.dismissed = { [weak self] shared in
|
||||||
if shared {
|
if shared {
|
||||||
self?.commitPurposefulAction()
|
self?.commitPurposefulAction()
|
||||||
|
|||||||
@ -993,9 +993,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
initialSearchLocation = .index(MessageIndex.absoluteUpperBound())
|
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 {
|
} 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 {
|
} else {
|
||||||
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0)
|
self.chatHistoryLocationValue = ChatHistoryLocationInput(content: .Initial(count: historyMessageCount), id: 0)
|
||||||
}
|
}
|
||||||
@ -1815,9 +1815,9 @@ public final class ChatHistoryListNodeImpl: ListView, ChatHistoryNode, ChatHisto
|
|||||||
initialSearchLocation = .index(.absoluteUpperBound())
|
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 {
|
} 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 {
|
} else if var chatHistoryLocation = strongSelf.chatHistoryLocationValue {
|
||||||
chatHistoryLocation.id += 1
|
chatHistoryLocation.id += 1
|
||||||
strongSelf.chatHistoryLocationValue = chatHistoryLocation
|
strongSelf.chatHistoryLocationValue = chatHistoryLocation
|
||||||
|
|||||||
@ -282,7 +282,7 @@ func chatHistoryViewForLocation(
|
|||||||
|
|
||||||
preloaded = true
|
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, _):
|
case let .Navigation(index, anchorIndex, count, _):
|
||||||
@ -415,7 +415,7 @@ func fetchAndPreloadReplyThreadInfo(context: AccountContext, subject: ReplyThrea
|
|||||||
case .automatic:
|
case .automatic:
|
||||||
if let atMessageId = atMessageId {
|
if let atMessageId = atMessageId {
|
||||||
input = ChatHistoryLocationInput(
|
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
|
id: 0
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -87,7 +87,7 @@ public enum ParsedInternalUrl {
|
|||||||
|
|
||||||
case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?)
|
case peer(UrlPeerReference, ParsedInternalPeerUrlParameter?)
|
||||||
case peerId(PeerId)
|
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 stickerPack(name: String, type: StickerPackUrlType)
|
||||||
case invoice(String)
|
case invoice(String)
|
||||||
case join(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 {
|
if let channelId = Int64(pathComponents[1]), let messageId = Int32(pathComponents[2]), channelId > 0 {
|
||||||
var threadId: Int32?
|
var threadId: Int32?
|
||||||
var timecode: Double?
|
var timecode: Double?
|
||||||
|
var taskId: Int32?
|
||||||
if let queryItems = components.queryItems {
|
if let queryItems = components.queryItems {
|
||||||
for queryItem in queryItems {
|
for queryItem in queryItems {
|
||||||
if let value = queryItem.value {
|
if let value = queryItem.value {
|
||||||
@ -557,11 +558,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
|||||||
if timestampValue != 0 {
|
if timestampValue != 0 {
|
||||||
timecode = Double(timestampValue)
|
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 {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -574,6 +579,7 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
|||||||
} else if pathComponents.count == 4 && pathComponents[0] == "c" {
|
} 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 {
|
if let channelId = Int64(pathComponents[1]), let threadId = Int32(pathComponents[2]), let messageId = Int32(pathComponents[3]), channelId > 0 {
|
||||||
var timecode: Double?
|
var timecode: Double?
|
||||||
|
var taskId: Int32?
|
||||||
if let queryItems = components.queryItems {
|
if let queryItems = components.queryItems {
|
||||||
for queryItem in queryItems {
|
for queryItem in queryItems {
|
||||||
if let value = queryItem.value {
|
if let value = queryItem.value {
|
||||||
@ -582,11 +588,15 @@ public func parseInternalUrl(sharedContext: SharedAccountContext, context: Accou
|
|||||||
if timestampValue != 0 {
|
if timestampValue != 0 {
|
||||||
timecode = Double(timestampValue)
|
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 {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -935,7 +945,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
|||||||
return .single(.result(.peer(nil, .info(nil))))
|
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))
|
return context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: messageId.peerId))
|
||||||
|> mapToSignal { peer -> Signal<ResolveInternalUrlResult, NoError> in
|
|> mapToSignal { peer -> Signal<ResolveInternalUrlResult, NoError> in
|
||||||
let foundPeer: Signal<EnginePeer?, NoError>
|
let foundPeer: Signal<EnginePeer?, NoError>
|
||||||
@ -1018,7 +1028,7 @@ private func resolveInternalUrl(context: AccountContext, url: ParsedInternalUrl)
|
|||||||
return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId))
|
return .result(.replyThreadMessage(replyThreadMessage: result, messageId: messageId))
|
||||||
})
|
})
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
return .single(.result(.inaccessiblePeer))
|
return .single(.result(.inaccessiblePeer))
|
||||||
|
|||||||
@ -1753,6 +1753,8 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
|||||||
self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string)
|
self?.webView?.sendEvent(name: "secure_storage_cleared", data: data.string)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
case "web_app_hide_keyboard":
|
||||||
|
self.view.window?.endEditing(true)
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user