[WIP] Business

This commit is contained in:
Isaac 2024-02-26 18:43:15 +04:00
parent 50339b3c4c
commit 0ba75f81de
18 changed files with 520 additions and 161 deletions

View File

@ -1441,7 +1441,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
self.textNode = TextNodeWithEntities()
self.textNode.textNode.isUserInteractionEnabled = false
self.textNode.textNode.displaysAsynchronously = true
self.textNode.textNode.layer.anchorPoint = CGPoint()
self.textNode.textNode.anchorPoint = CGPoint()
self.inputActivitiesNode = ChatListInputActivitiesNode()
self.inputActivitiesNode.isUserInteractionEnabled = false

View File

@ -314,19 +314,37 @@ public final class TelegramBusinessHours: Equatable, Codable {
public func splitIntoWeekDays() -> [WeekDay] {
var mappedDays: [[WorkingTimeInterval]] = Array(repeating: [], count: 7)
var weekMinutes = IndexSet()
for interval in self.weeklyTimeIntervals {
let startDayIndex = interval.startMinute / (24 * 60)
if startDayIndex < 0 || startDayIndex >= 7 {
continue
weekMinutes.insert(integersIn: interval.startMinute ..< interval.endMinute)
}
for i in 0 ..< mappedDays.count {
let dayRange = i * 24 * 60 ..< (i + 1) * 24 * 60
var removeMinutes = IndexSet()
inner: for range in weekMinutes.rangeView {
if range.lowerBound >= dayRange.upperBound {
break inner
} else {
let clippedRange: Range<Int>
if range.lowerBound == dayRange.lowerBound {
clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound)
} else {
clippedRange = range.lowerBound ..< min(range.upperBound, dayRange.upperBound + 12 * 60)
}
let startTimeInsideDay = clippedRange.lowerBound - i * (24 * 60)
let endTimeInsideDay = clippedRange.upperBound - i * (24 * 60)
mappedDays[i].append(WorkingTimeInterval(
startMinute: startTimeInsideDay,
endMinute: endTimeInsideDay
))
removeMinutes.insert(integersIn: clippedRange)
}
}
let startTimeInsideDay = interval.startMinute - startDayIndex * (24 * 60)
let endTimeInsideDay = interval.endMinute - startDayIndex * (24 * 60)
mappedDays[startDayIndex].append(WorkingTimeInterval(
startMinute: startTimeInsideDay,
endMinute: endTimeInsideDay
))
weekMinutes.subtract(removeMinutes)
}
return mappedDays.map { day -> WeekDay in

View File

@ -1191,7 +1191,7 @@ public extension TelegramEngine {
}
var selectedMedia: EngineMedia
if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), !preferHighQuality {
if let alternativeMedia = itemAndPeer.item.alternativeMedia.flatMap(EngineMedia.init), (!preferHighQuality && !itemAndPeer.item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = EngineMedia(media)

View File

@ -66,6 +66,7 @@ public final class ListActionItemComponent: Component {
public let theme: PresentationTheme
public let title: AnyComponent<Empty>
public let contentInsets: UIEdgeInsets
public let leftIcon: AnyComponentWithIdentity<Empty>?
public let icon: Icon?
public let accessory: Accessory?
@ -74,6 +75,7 @@ public final class ListActionItemComponent: Component {
public init(
theme: PresentationTheme,
title: AnyComponent<Empty>,
contentInsets: UIEdgeInsets = UIEdgeInsets(top: 12.0, left: 0.0, bottom: 12.0, right: 0.0),
leftIcon: AnyComponentWithIdentity<Empty>? = nil,
icon: Icon? = nil,
accessory: Accessory? = .arrow,
@ -81,6 +83,7 @@ public final class ListActionItemComponent: Component {
) {
self.theme = theme
self.title = title
self.contentInsets = contentInsets
self.leftIcon = leftIcon
self.icon = icon
self.accessory = accessory
@ -94,6 +97,9 @@ public final class ListActionItemComponent: Component {
if lhs.title != rhs.title {
return false
}
if lhs.contentInsets != rhs.contentInsets {
return false
}
if lhs.leftIcon != rhs.leftIcon {
return false
}
@ -172,8 +178,6 @@ public final class ListActionItemComponent: Component {
let themeUpdated = component.theme !== previousComponent?.theme
let verticalInset: CGFloat = 12.0
var contentLeftInset: CGFloat = 16.0
let contentRightInset: CGFloat
switch component.accessory {
@ -186,7 +190,7 @@ public final class ListActionItemComponent: Component {
}
var contentHeight: CGFloat = 0.0
contentHeight += verticalInset
contentHeight += component.contentInsets.top
if component.leftIcon != nil {
contentLeftInset += 46.0
@ -198,7 +202,7 @@ public final class ListActionItemComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - contentLeftInset - contentRightInset, height: availableSize.height)
)
let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: verticalInset), size: titleSize)
let titleFrame = CGRect(origin: CGPoint(x: contentLeftInset, y: contentHeight), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
@ -208,7 +212,7 @@ public final class ListActionItemComponent: Component {
}
contentHeight += titleSize.height
contentHeight += verticalInset
contentHeight += component.contentInsets.bottom
if let iconValue = component.icon {
if previousComponent?.icon?.component.id != iconValue.component.id, let icon = self.icon {

View File

@ -208,7 +208,7 @@ public final class ListMultilineTextFieldItemComponent: Component {
containerSize: availableSize
)
let size = textFieldSize
let size = CGSize(width: textFieldSize.width, height: textFieldSize.height - 1.0)
let textFieldFrame = CGRect(origin: CGPoint(), size: textFieldSize)
if let textFieldView = self.textField.view {

View File

@ -139,6 +139,7 @@ swift_library(
"//submodules/TelegramUI/Components/Settings/BoostLevelIconComponent",
"//submodules/Components/MultilineTextComponent",
"//submodules/TelegramUI/Components/Settings/PeerNameColorItem",
"//submodules/TelegramUI/Components/PlainButtonComponent",
],
visibility = [
"//visibility:public",

View File

@ -11,8 +11,9 @@ import TelegramCore
import ComponentFlow
import MultilineTextComponent
import BundleIconComponent
import PlainButtonComponent
private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> String {
private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay, offsetMinutes: Int) -> String {
var businessHoursText: String = ""
switch day {
case .open:
@ -26,6 +27,8 @@ private func dayBusinessHoursText(_ day: TelegramBusinessHours.WeekDay) -> Strin
var resultText: String = ""
for range in intervals {
let range = TelegramBusinessHours.WorkingTimeInterval(startMinute: range.startMinute + offsetMinutes, endMinute: range.endMinute + offsetMinutes)
if !resultText.isEmpty {
resultText.append("\n")
}
@ -47,13 +50,13 @@ final class PeerInfoScreenBusinessHoursItem: PeerInfoScreenItem {
let id: AnyHashable
let label: String
let businessHours: TelegramBusinessHours
let requestLayout: () -> Void
let requestLayout: (Bool) -> Void
init(
id: AnyHashable,
label: String,
businessHours: TelegramBusinessHours,
requestLayout: @escaping () -> Void
requestLayout: @escaping (Bool) -> Void
) {
self.id = id
self.label = label
@ -79,6 +82,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
private let labelNode: ImmediateTextNode
private let currentStatusText = ComponentView<Empty>()
private let currentDayText = ComponentView<Empty>()
private var timezoneSwitchButton: ComponentView<Empty>?
private var dayTitles: [ComponentView<Empty>] = []
private var dayValues: [ComponentView<Empty>] = []
private let arrowIcon = ComponentView<Empty>()
@ -90,6 +94,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
private var item: PeerInfoScreenBusinessHoursItem?
private var theme: PresentationTheme?
private var currentTimezone: TimeZone
private var displayLocalTimezone: Bool = false
private var cachedDays: [TelegramBusinessHours.WeekDay] = []
private var cachedWeekMinuteSet = IndexSet()
@ -115,6 +121,8 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
self.activateArea = AccessibilityAreaNode()
self.currentTimezone = TimeZone.current
super.init()
self.addSubnode(self.bottomSeparatorNode)
@ -179,7 +187,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
switch gesture {
case .tap, .longTap:
self.isExpanded = !self.isExpanded
self.item?.requestLayout()
self.item?.requestLayout(true)
default:
break
}
@ -255,11 +263,16 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
let currentHour = currentCalendar.component(.hour, from: currentDate)
let currentWeekMinute = currentDayIndex * 24 * 60 + currentHour * 60 + currentMinute
var timezoneOffsetMinutes: Int = 0
if self.displayLocalTimezone {
timezoneOffsetMinutes = (self.currentTimezone.secondsFromGMT() - currentCalendar.timeZone.secondsFromGMT()) / 60
}
let isOpen = self.cachedWeekMinuteSet.contains(currentWeekMinute)
//TODO:localize
let openStatusText = isOpen ? "Open" : "Closed"
var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex]) : " "
var currentDayStatusText = currentDayIndex >= 0 && currentDayIndex < businessDays.count ? dayBusinessHoursText(businessDays[currentDayIndex], offsetMinutes: timezoneOffsetMinutes) : " "
if !isOpen {
for range in self.cachedWeekMinuteSet.rangeView {
@ -325,6 +338,61 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
environment: {},
containerSize: CGSize(width: width - sideInset - dayRightInset, height: 100.0)
)
var timezoneSwitchButtonSize: CGSize?
if item.businessHours.timezoneId != self.currentTimezone.identifier {
let timezoneSwitchButton: ComponentView<Empty>
if let current = self.timezoneSwitchButton {
timezoneSwitchButton = current
} else {
timezoneSwitchButton = ComponentView()
self.timezoneSwitchButton = timezoneSwitchButton
}
let timezoneSwitchTitle: String
//TODO:localize
if self.displayLocalTimezone {
timezoneSwitchTitle = "my time"
} else {
timezoneSwitchTitle = "local time"
}
timezoneSwitchButtonSize = timezoneSwitchButton.update(
transition: .immediate,
component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: timezoneSwitchTitle, font: Font.regular(12.0), textColor: presentationData.theme.list.itemAccentColor))
)),
background: AnyComponent(RoundedRectangle(
color: presentationData.theme.list.itemAccentColor.withMultipliedAlpha(0.1),
cornerRadius: nil
)),
effectAlignment: .center,
contentInsets: UIEdgeInsets(top: 1.0, left: 7.0, bottom: 2.0, right: 7.0),
action: { [weak self] in
guard let self else {
return
}
self.displayLocalTimezone = !self.displayLocalTimezone
self.item?.requestLayout(false)
},
animateAlpha: true,
animateScale: false,
animateContents: false
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
} else {
if let timezoneSwitchButton = self.timezoneSwitchButton {
self.timezoneSwitchButton = nil
timezoneSwitchButton.view?.removeFromSuperview()
}
}
let timezoneSwitchButtonSpacing: CGFloat = 3.0
if timezoneSwitchButtonSize != nil {
topOffset -= 20.0
}
let currentDayTextFrame = CGRect(origin: CGPoint(x: width - dayRightInset - currentDayTextSize.width, y: topOffset), size: currentDayTextSize)
if let currentDayTextView = self.currentDayText.view {
if currentDayTextView.superview == nil {
@ -337,6 +405,20 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
topOffset += max(currentStatusTextSize.height, currentDayTextSize.height)
if let timezoneSwitchButtonView = self.timezoneSwitchButton?.view, let timezoneSwitchButtonSize {
topOffset += timezoneSwitchButtonSpacing
var timezoneSwitchButtonTransition = transition
if timezoneSwitchButtonView.superview == nil {
timezoneSwitchButtonTransition = .immediate
self.contextSourceNode.contentNode.view.addSubview(timezoneSwitchButtonView)
}
let timezoneSwitchButtonFrame = CGRect(origin: CGPoint(x: width - dayRightInset - timezoneSwitchButtonSize.width, y: topOffset), size: timezoneSwitchButtonSize)
timezoneSwitchButtonTransition.updateFrame(view: timezoneSwitchButtonView, frame: timezoneSwitchButtonFrame)
topOffset += timezoneSwitchButtonSize.height
}
let daySpacing: CGFloat = 15.0
var dayHeights: CGFloat = 0.0
@ -383,7 +465,7 @@ private final class PeerInfoScreenBusinessHoursItemNode: PeerInfoScreenItemNode
dayTitleValue = " "
}
let businessHoursText = dayBusinessHoursText(businessDays[i])
let businessHoursText = dayBusinessHoursText(businessDays[i], offsetMinutes: timezoneOffsetMinutes)
let dayTitleSize = dayTitle.update(
transition: .immediate,

View File

@ -1166,8 +1166,8 @@ private func infoItems(data: PeerInfoScreenData?, context: AccountContext, prese
if let businessHours = cachedData.businessHours {
//TODO:localize
items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: {
interaction.requestLayout(true)
items[.peerInfo]!.append(PeerInfoScreenBusinessHoursItem(id: 300, label: "business hours", businessHours: businessHours, requestLayout: { animated in
interaction.requestLayout(animated)
}))
}

View File

@ -81,6 +81,7 @@ final class BusinessDaySetupScreenComponent: Component {
private(set) var isOpen: Bool = false
private(set) var ranges: [BusinessHoursSetupScreenComponent.WorkingHourRange] = []
private var intersectingRanges = Set<Int>()
private var nextRangeId: Int = 0
override init(frame: CGRect) {
@ -116,7 +117,25 @@ final class BusinessDaySetupScreenComponent: Component {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
return true
guard let component = self.component else {
return true
}
if self.intersectingRanges.isEmpty {
return true
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Business hours are intersecting. Reset?", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .defaultAction, title: "Reset", action: {
complete()
})
]), in: .window(.root))
return false
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
@ -191,6 +210,26 @@ final class BusinessDaySetupScreenComponent: Component {
private func validateRanges() {
self.ranges.sort(by: { $0.startMinute < $1.startMinute })
self.intersectingRanges.removeAll()
for i in 0 ..< self.ranges.count {
var minuteSet = IndexSet()
inner: for j in 0 ..< self.ranges.count {
if i == j {
continue inner
}
let range = self.ranges[j]
let rangeMinutes = range.startMinute ..< range.endMinute
minuteSet.insert(integersIn: rangeMinutes)
}
let range = self.ranges[i]
let rangeMinutes = range.startMinute ..< range.endMinute
if minuteSet.intersects(integersIn: rangeMinutes) {
self.intersectingRanges.insert(range.id)
}
}
}
func update(component: BusinessDaySetupScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
@ -260,7 +299,7 @@ final class BusinessDaySetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let sectionSpacing: CGFloat = 24.0
let _ = bottomContentInset
let _ = sectionSpacing
@ -335,80 +374,47 @@ final class BusinessDaySetupScreenComponent: Component {
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
var rangeSectionItems: [AnyComponentWithIdentity<Empty>] = []
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Opening time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
for i in 0 ..< 2 {
let isOpenTime = i == 0
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: isOpenTime ? "Opening time" : "Closing Time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: isOpenTime ? startText : endText, font: Font.regular(17.0), textColor: self.intersectingRanges.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemPrimaryTextColor))
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: startText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
self.openRangeDateSetup(rangeId: rangeId, isStartTime: isOpenTime)
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: true)
}
))))
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
AnyComponentWithIdentity(id: AnyHashable(0), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: "Closing time",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
icon: ListActionItemComponent.Icon(component: AnyComponentWithIdentity(id: 0, component: AnyComponent(PlainButtonComponent(
content: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: endText, font: Font.regular(17.0), textColor: environment.theme.list.itemPrimaryTextColor))
)),
background: AnyComponent(RoundedRectangle(color: environment.theme.list.itemPrimaryTextColor.withMultipliedAlpha(0.1), cornerRadius: 6.0)),
effectAlignment: .center,
minSize: nil,
contentInsets: UIEdgeInsets(top: 7.0, left: 8.0, bottom: 7.0, right: 8.0),
action: { [weak self] in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
},
animateAlpha: true,
animateScale: false
))), insets: .custom(UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0)), allowUserInteraction: true),
accessory: nil,
action: { [weak self] _ in
guard let self else {
return
}
self.openRangeDateSetup(rangeId: rangeId, isStartTime: false)
}
))))
))))
}
rangeSectionItems.append(AnyComponentWithIdentity(id: rangeSectionItems.count, component: AnyComponent(ListActionItemComponent(
theme: environment.theme,
title: AnyComponent(VStack([
@ -519,13 +525,13 @@ final class BusinessDaySetupScreenComponent: Component {
text: .plain(NSAttributedString(
string: "Add a Set of Hours",
font: Font.regular(presentationData.listsFontSize.baseDisplaySize),
textColor: environment.theme.list.itemPrimaryTextColor
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 1
))),
], alignment: .left, spacing: 2.0)),
leftIcon: AnyComponentWithIdentity(id: 0, component: AnyComponent(BundleIconComponent(
name: "Chat List/AddIcon",
name: "Item List/AddTimeIcon",
tintColor: environment.theme.list.itemAccentColor
))),
accessory: nil,
@ -619,7 +625,7 @@ final class BusinessDaySetupScreenComponent: Component {
final class BusinessDaySetupScreen: ViewControllerComponentContainer {
private let context: AccountContext
private let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void
fileprivate let updateDay: (BusinessHoursSetupScreenComponent.Day) -> Void
init(context: AccountContext, dayIndex: Int, day: BusinessHoursSetupScreenComponent.Day, updateDay: @escaping (BusinessHoursSetupScreenComponent.Day) -> Void) {
self.context = context
@ -647,9 +653,12 @@ final class BusinessDaySetupScreen: ViewControllerComponentContainer {
return true
}
self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil))
return componentView.attemptNavigation(complete: complete)
if componentView.attemptNavigation(complete: complete) {
self.updateDay(BusinessHoursSetupScreenComponent.Day(ranges: componentView.isOpen ? componentView.ranges : nil))
return true
} else {
return false
}
}
}

View File

@ -22,6 +22,32 @@ import LocationUI
import TelegramStringFormatting
import TimezoneSelectionScreen
private func wrappedMinuteRange(range: Range<Int>, dayIndexOffset: Int = 0) -> IndexSet {
let mappedRange = (range.lowerBound + dayIndexOffset * 24 * 60) ..< (range.upperBound + dayIndexOffset * 24 * 60)
var result = IndexSet()
if mappedRange.upperBound > 7 * 24 * 60 {
result.insert(integersIn: mappedRange.lowerBound ..< 7 * 24 * 60)
result.insert(integersIn: 0 ..< (mappedRange.upperBound - 7 * 24 * 60))
} else {
result.insert(integersIn: mappedRange)
}
return result
}
private func getDayRanges(days: [BusinessHoursSetupScreenComponent.Day], index: Int) -> [BusinessHoursSetupScreenComponent.WorkingHourRange] {
let day = days[index]
if let ranges = day.ranges {
if ranges.isEmpty {
return [BusinessHoursSetupScreenComponent.WorkingHourRange(id: 0, startMinute: 0, endMinute: 24 * 60)]
} else {
return ranges
}
} else {
return []
}
}
final class BusinessHoursSetupScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
@ -68,6 +94,16 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
struct DayRangeIndex: Hashable {
var day: Int
var id: Int
init(day: Int, id: Int) {
self.day = day
self.id = id
}
}
struct Day: Equatable {
var ranges: [WorkingHourRange]?
@ -83,7 +119,7 @@ final class BusinessHoursSetupScreenComponent: Component {
var timezoneId: String
private(set) var days: [Day]
private(set) var intersectingDays = Set<Int>()
private(set) var intersectingRanges = Set<DayRangeIndex>()
init(timezoneId: String, days: [Day]) {
self.timezoneId = timezoneId
@ -111,13 +147,45 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
if let value = try? self.asBusinessHours() {
if value != businessHours {
assertionFailure("Inconsistent representation")
}
}
self.validate()
}
mutating func validate() {
self.intersectingDays.removeAll()
self.intersectingRanges.removeAll()
for dayIndex in 0 ..< self.days.count {
var otherDaysMinutes = IndexSet()
inner: for otherDayIndex in 0 ..< self.days.count {
if dayIndex == otherDayIndex {
continue inner
}
for range in getDayRanges(days: self.days, index: otherDayIndex) {
otherDaysMinutes.formUnion(wrappedMinuteRange(range: range.startMinute ..< range.endMinute, dayIndexOffset: otherDayIndex))
}
}
let dayRanges = getDayRanges(days: self.days, index: dayIndex)
for i in 0 ..< dayRanges.count {
var currentDayOtherMinutes = IndexSet()
inner: for j in 0 ..< dayRanges.count {
if i == j {
continue inner
}
currentDayOtherMinutes.formUnion(wrappedMinuteRange(range: dayRanges[j].startMinute ..< dayRanges[j].endMinute, dayIndexOffset: dayIndex))
}
let currentDayIndices = wrappedMinuteRange(range: dayRanges[i].startMinute ..< dayRanges[i].endMinute, dayIndexOffset: dayIndex)
if !otherDaysMinutes.intersection(currentDayIndices).isEmpty || !currentDayOtherMinutes.intersection(currentDayIndices).isEmpty {
self.intersectingRanges.insert(DayRangeIndex(day: dayIndex, id: dayRanges[i].id))
}
}
}
}
mutating func update(days: [Day]) {
@ -140,13 +208,7 @@ final class BusinessHoursSetupScreenComponent: Component {
for range in effectiveRanges {
let minuteRange: Range<Int> = (dayStartMinute + range.startMinute) ..< (dayStartMinute + range.endMinute)
var wrappedMinutes = IndexSet()
if minuteRange.upperBound > 7 * 24 * 60 {
wrappedMinutes.insert(integersIn: minuteRange.lowerBound ..< 7 * 24 * 60)
wrappedMinutes.insert(integersIn: 0 ..< (7 * 24 * 60 - minuteRange.upperBound))
} else {
wrappedMinutes.insert(integersIn: minuteRange)
}
let wrappedMinutes = wrappedMinuteRange(range: minuteRange)
if !filledMinutes.intersection(wrappedMinutes).isEmpty {
throw ValidationError.intersectingRanges
@ -156,7 +218,20 @@ final class BusinessHoursSetupScreenComponent: Component {
}
}
return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mappedIntervals)
var mergedIntervals: [TelegramBusinessHours.WorkingTimeInterval] = []
for interval in mappedIntervals {
if mergedIntervals.isEmpty {
mergedIntervals.append(interval)
} else {
if mergedIntervals[mergedIntervals.count - 1].endMinute >= interval.startMinute {
mergedIntervals[mergedIntervals.count - 1] = TelegramBusinessHours.WorkingTimeInterval(startMinute: mergedIntervals[mergedIntervals.count - 1].startMinute, endMinute: interval.endMinute)
} else {
mergedIntervals.append(interval)
}
}
}
return TelegramBusinessHours(timezoneId: self.timezoneId, weeklyTimeIntervals: mergedIntervals)
}
}
@ -231,7 +306,6 @@ final class BusinessHoursSetupScreenComponent: Component {
return true
} catch let error {
let _ = error
//TODO:localize
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
@ -359,7 +433,7 @@ final class BusinessHoursSetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let sectionSpacing: CGFloat = 30.0
let _ = bottomContentInset
let _ = sectionSpacing
@ -370,14 +444,14 @@ final class BusinessHoursSetupScreenComponent: Component {
let iconSize = self.icon.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: "", font: Font.semibold(90.0), textColor: environment.theme.rootController.navigationBar.primaryTextColor)),
horizontalAlignment: .center
component: AnyComponent(LottieComponent(
content: LottieComponent.AppBundleContent(name: "BusinessHoursEmoji"),
loop: true
)),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 10.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
@ -386,7 +460,7 @@ final class BusinessHoursSetupScreenComponent: Component {
iconView.bounds = CGRect(origin: CGPoint(), size: iconFrame.size)
}
contentHeight += 129.0
contentHeight += 126.0
//TODO:localize
let subtitleString = NSMutableAttributedString(attributedString: parseMarkdownIntoAttributedString("Turn this on to show your opening hours schedule to your customers.", attributes: MarkdownAttributes(
@ -506,28 +580,42 @@ final class BusinessHoursSetupScreenComponent: Component {
title = " "
}
let subtitle: String
let subtitle = NSMutableAttributedString()
var invalidIndices: [Int] = []
let effectiveDayRanges = getDayRanges(days: self.daysState.days, index: dayIndex)
for range in effectiveDayRanges {
if self.daysState.intersectingRanges.contains(DayRangeIndex(day: dayIndex, id: range.id)) {
invalidIndices.append(range.id)
}
}
let subtitleFont = Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0))
if let ranges = self.daysState.days[dayIndex].ranges {
if ranges.isEmpty {
subtitle = "Open 24 Hours"
subtitle.append(NSAttributedString(string: "Open 24 Hours", font: subtitleFont, textColor: invalidIndices.contains(0) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor))
} else {
var resultText: String = ""
for range in ranges {
if !resultText.isEmpty {
resultText.append(", ")
}
for i in 0 ..< ranges.count {
let range = ranges[i]
let startHours = clipMinutes(range.startMinute) / 60
let startMinutes = clipMinutes(range.startMinute) % 60
let startText = stringForShortTimestamp(hours: Int32(startHours), minutes: Int32(startMinutes), dateTimeFormat: PresentationDateTimeFormat())
let endHours = clipMinutes(range.endMinute) / 60
let endMinutes = clipMinutes(range.endMinute) % 60
let endText = stringForShortTimestamp(hours: Int32(endHours), minutes: Int32(endMinutes), dateTimeFormat: PresentationDateTimeFormat())
resultText.append("\(startText)\u{00a0}- \(endText)")
var rangeString = "\(startText)\u{00a0}- \(endText)"
if i != ranges.count - 1 {
rangeString.append(", ")
}
subtitle.append(NSAttributedString(string: rangeString, font: subtitleFont, textColor: invalidIndices.contains(range.id) ? environment.theme.list.itemDestructiveColor : environment.theme.list.itemAccentColor))
}
subtitle = resultText
}
} else {
subtitle = "Closed"
subtitle.append(NSAttributedString(string: "Closed", font: subtitleFont, textColor: environment.theme.list.itemAccentColor))
}
daysSectionItems.append(AnyComponentWithIdentity(id: dayIndex, component: AnyComponent(ListActionItemComponent(
@ -542,14 +630,11 @@ final class BusinessHoursSetupScreenComponent: Component {
maximumNumberOfLines: 1
))),
AnyComponentWithIdentity(id: AnyHashable(1), component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(
string: subtitle,
font: Font.regular(floor(presentationData.listsFontSize.baseDisplaySize * 15.0 / 17.0)),
textColor: environment.theme.list.itemAccentColor
)),
maximumNumberOfLines: 5
text: .plain(subtitle),
maximumNumberOfLines: 20
)))
], alignment: .left, spacing: 2.0)),
], alignment: .left, spacing: 3.0)),
contentInsets: UIEdgeInsets(top: 9.0, left: 0.0, bottom: 10.0, right: 0.0),
accessory: .toggle(ListActionItemComponent.Toggle(style: .regular, isOn: day.ranges != nil, action: { [weak self] _ in
guard let self else {
return

View File

@ -109,20 +109,38 @@ final class BusinessLocationSetupScreenComponent: Component {
}
func attemptNavigation(complete: @escaping () -> Void) -> Bool {
if let component = self.component {
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
guard let component = self.component else {
return true
}
var address = ""
if let textView = self.addressSection.findTaggedView(tag: self.textFieldTag) as? ListMultilineTextFieldItemComponent.View {
address = textView.currentText
}
var businessLocation: TelegramBusinessLocation?
if !address.isEmpty || self.mapCoordinates != nil {
businessLocation = TelegramBusinessLocation(address: address, coordinates: self.mapCoordinates)
}
if businessLocation != nil && address.isEmpty {
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
//TODO:localize
self.environment?.controller()?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: presentationData), title: nil, text: "Address can't be empty.", actions: [
TextAlertAction(type: .genericAction, title: "Cancel", action: {
}),
TextAlertAction(type: .destructiveAction, title: "Delete", action: {
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: nil).startStandalone()
complete()
})
]), in: .window(.root))
return false
}
let _ = component.context.engine.accountData.updateAccountBusinessLocation(businessLocation: businessLocation).startStandalone()
return true
}
@ -228,10 +246,7 @@ final class BusinessLocationSetupScreenComponent: Component {
let bottomContentInset: CGFloat = 24.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
let sectionSpacing: CGFloat = 32.0
let _ = bottomContentInset
let _ = sectionSpacing
let sectionSpacing: CGFloat = 24.0
var contentHeight: CGFloat = 0.0
@ -246,7 +261,7 @@ final class BusinessLocationSetupScreenComponent: Component {
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 2.0), size: iconSize)
let iconFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - iconSize.width) * 0.5), y: contentHeight + 11.0), size: iconSize)
if let iconView = self.icon.view {
if iconView.superview == nil {
self.scrollView.addSubview(iconView)
@ -304,6 +319,7 @@ final class BusinessLocationSetupScreenComponent: Component {
contentHeight += subtitleSize.height
contentHeight += 27.0
//TODO:localize
var addressSectionItems: [AnyComponentWithIdentity<Empty>] = []
addressSectionItems.append(AnyComponentWithIdentity(id: 0, component: AnyComponent(ListMultilineTextFieldItemComponent(
context: component.context,

View File

@ -34,7 +34,22 @@ private struct TimezoneListEntry: Comparable, Identifiable {
}
func item(presentationData: PresentationData, searchMode: Bool, action: @escaping (String) -> Void) -> ListViewItem {
return ItemListActionItem(presentationData: ItemListPresentationData(presentationData), title: self.title, kind: .neutral, alignment: .natural, sectionId: 0, style: .plain, action: {
let hours = abs(self.offset / (60 * 60))
let minutes = abs(self.offset % (60 * 60)) / 60
let offsetString: String
if minutes == 0 {
offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours)"
} else {
let minutesString: String
if minutes < 10 {
minutesString = "0\(minutes)"
} else {
minutesString = "\(minutes)"
}
offsetString = "UTC \(self.offset >= 0 ? "+" : "-")\(hours):\(minutesString)"
}
return ItemListDisclosureItem(presentationData: ItemListPresentationData(presentationData), title: self.title, label: offsetString, sectionId: 0, style: .plain, disclosureStyle: .none, action: {
action(self.id)
})
}

View File

@ -990,7 +990,7 @@ public final class StoryContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
@ -1642,7 +1642,7 @@ public final class PeerStoryListContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let alternativeMedia = item.alternativeMedia, !preferHighQualityStories {
if let alternativeMedia = item.alternativeMedia, (!preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media
@ -2880,7 +2880,7 @@ public final class RepostStoriesContentContextImpl: StoryContentContext {
}
var selectedMedia: EngineMedia
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, !slice.additionalPeerData.preferHighQualityStories {
if let slice = stateValue.slice, let alternativeMedia = item.alternativeMedia, (!slice.additionalPeerData.preferHighQualityStories && !item.isMy) {
selectedMedia = alternativeMedia
} else {
selectedMedia = item.media

View File

@ -593,7 +593,7 @@ final class StoryItemContentComponent: Component {
let selectedMedia: EngineMedia
var messageMedia: EngineMedia?
if !component.preferHighQuality, let alternativeMedia = component.item.alternativeMedia {
if !component.preferHighQuality, !component.item.isMy, let alternativeMedia = component.item.alternativeMedia {
selectedMedia = alternativeMedia
switch alternativeMedia {

View File

@ -6857,7 +6857,7 @@ public final class StoryItemSetContainerComponent: Component {
})))
}
if case let .file(file) = component.slice.item.storyItem.media, file.isVideo {
if !component.slice.item.storyItem.isMy, case let .file(file) = component.slice.item.storyItem.media, file.isVideo {
let isHq = component.slice.additionalPeerData.preferHighQualityStories
items.append(.action(ContextMenuActionItem(text: isHq ? component.strings.Story_ContextMenuSD : component.strings.Story_ContextMenuHD, icon: { theme in
if isHq {

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "addclock_30.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,117 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.334961 4.334961 cm
0.000000 0.000000 0.000000 scn
1.330000 10.665078 m
1.330000 15.820656 5.509422 20.000078 10.665000 20.000078 c
15.820579 20.000078 20.000000 15.820656 20.000000 10.665078 c
20.000000 5.509500 15.820579 1.330078 10.665000 1.330078 c
10.024749 1.330078 9.400214 1.394436 8.797290 1.516823 c
8.437361 1.589886 8.086353 1.357334 8.013291 0.997406 c
7.940229 0.637478 8.172781 0.286469 8.532710 0.213406 c
9.222226 0.073441 9.935388 0.000076 10.665000 0.000076 c
16.555117 0.000076 21.330002 4.774961 21.330002 10.665078 c
21.330002 16.555195 16.555117 21.330078 10.665000 21.330078 c
4.774883 21.330078 0.000000 16.555195 0.000000 10.665078 c
0.000000 9.935467 0.073363 9.222304 0.213327 8.532788 c
0.286389 8.172859 0.637397 7.940308 0.997326 8.013370 c
1.357255 8.086432 1.589807 8.437439 1.516745 8.797368 c
1.394358 9.400291 1.330000 10.024826 1.330000 10.665078 c
h
11.330000 17.165077 m
11.330000 17.532347 11.032269 17.830078 10.665000 17.830078 c
10.297730 17.830078 10.000000 17.532347 10.000000 17.165077 c
10.000000 11.114144 l
10.000000 10.638557 10.203376 10.185671 10.558834 9.869707 c
14.723198 6.168051 l
14.997698 5.924050 15.418027 5.948775 15.662027 6.223276 c
15.906028 6.497777 15.881302 6.918105 15.606802 7.162106 c
11.442438 10.863762 l
11.370919 10.927334 11.330000 11.018456 11.330000 11.114144 c
11.330000 17.165077 l
h
f*
n
Q
q
1.000000 0.000000 -0.000000 1.000000 3.339844 3.334961 cm
0.000000 0.000000 0.000000 scn
5.330000 8.665077 m
5.330000 9.032347 5.032269 9.330078 4.665000 9.330078 c
4.297730 9.330078 4.000000 9.032347 4.000000 8.665077 c
4.000000 5.330077 l
0.665000 5.330077 l
0.297731 5.330077 0.000000 5.032347 0.000000 4.665077 c
0.000000 4.297808 0.297731 4.000077 0.665000 4.000077 c
4.000000 4.000077 l
4.000000 0.665077 l
4.000000 0.297808 4.297730 0.000077 4.665000 0.000077 c
5.032269 0.000077 5.330000 0.297808 5.330000 0.665077 c
5.330000 4.000077 l
8.665000 4.000077 l
9.032269 4.000077 9.330000 4.297808 9.330000 4.665077 c
9.330000 5.032347 9.032269 5.330077 8.665000 5.330077 c
5.330000 5.330077 l
5.330000 8.665077 l
h
f*
n
Q
endstream
endobj
3 0 obj
2167
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 30.000000 30.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002257 00000 n
0000002280 00000 n
0000002453 00000 n
0000002527 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2586
%%EOF