Commit f78c117b authored by Chaitanya Pandit's avatar Chaitanya Pandit Committed by Azat Alimov
Browse files

Resolved: Grouped timeline (without time)

parent 8490b893
......@@ -382,6 +382,8 @@ Person {
[renderer = timeline] {
timeProperty: dateCreated
detailLevel: month
grouped: true
}
}
......
......@@ -61,3 +61,88 @@ struct TimelineItemView_Previews: PreviewProvider {
TimelineItemView()
}
}
struct GroupedTimelineItemView: View {
var items: [ItemRecord]
var source: String? = nil
var cornerRadius: CGFloat = 5
var backgroundColor = Color(.systemGreen)
var foregroundColor: Color {
Color.white
}
var body: some View {
VStack(alignment: .leading) {
if let element = items.first {
let imageName = element.type == "Photo" ?
"photo.fill" :
element.type == "Message" ?
"bubble.left" :
"envelope"
let title =
element.type == "Photo" ?
element.type.titleCase() :
element.type == "Message" ?
"Conversation" :
"Email"
let subtitle = getSubtitle(data: items)
HStack(alignment: .center, spacing: 30) {
Image(systemName: imageName)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text(title)
.font(.headline)
.bold()
.lineLimit(1)
.foregroundColor(Color.blue)
Text(subtitle)
.font(.caption)
.lineLimit(2)
.foregroundColor(.gray)
source.map {
Text($0)
.font(.caption2)
.lineLimit(2)
.foregroundColor(.gray)
}
}
Spacer(minLength: 0)
Image(systemName: "greaterthan")
.foregroundColor(.blue)
}
.frame(maxWidth: .infinity, alignment: Alignment(horizontal: .leading, vertical: .top))
.fixedSize(horizontal: false, vertical: true)
.padding(20)
.foregroundColor(foregroundColor)
.background(Color.white)
.mask(
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
)
Divider()
}
}
}
func getSubtitle(data: [ItemRecord]) -> String {
var subTitle = ""
if let element = data.first {
subTitle = element.dateCreated.formatted("dd MMM YYYY")
if element.type == "Photo" {
subTitle = subTitle + " \(data.count) photo\(data.count > 1 ? "s" : "") #"
} else if element.type == "Message" {
subTitle = subTitle + " \(data.count) message\(data.count > 1 ? "s" : "") #"
} else {
subTitle = subTitle + " \(data.count) email\(data.count > 1 ? "s" : "") #"
}
}
return subTitle
}
}
struct GroupedTimelineItemView_Previews: PreviewProvider {
static var previews: some View {
GroupedTimelineItemView(items: [])
}
}
......@@ -29,6 +29,10 @@ struct TimelineRendererView: View {
viewContext.rendererDefinitionPropertyResolver.string("detailLevel").flatMap(TimelineRendererModel.DetailLevel.init) ?? .day
}
var grouped: Bool {
return viewContext.rendererDefinitionPropertyResolver.bool("grouped") ?? false
}
var mostRecentFirst: Bool {
viewContext.rendererDefinitionPropertyResolver.bool("recentFirst", defaultValue: true)
}
......@@ -36,8 +40,7 @@ struct TimelineRendererView: View {
let minSectionHeight: CGFloat = 40
func sections(withModel model: TimelineRendererModel) -> [ASSection<Date>] {
model.data.map { group in
return model.data.map { group in
ASSection<Date>(id: group.date,
data: group.items,
selectionMode: .selectSingle { index in
......@@ -49,8 +52,8 @@ struct TimelineRendererView: View {
).execute(sceneController: sceneController, context: viewContext.getCVUContext(), completion: nil)
}
else if let item = element.items.first,
let press = viewContext.nodePropertyResolver(for: item)?.action(key: "onPress") {
press.execute(sceneController: sceneController, context: viewContext.getCVUContext(forItem: item), completion: nil)
let press = viewContext.nodePropertyResolver(for: item)?.action(key: "onPress") {
press.execute(sceneController: sceneController, context: viewContext.getCVUContext(forItem: item), completion: nil)
}
}) { element, _ in
self.renderElement(element)
......@@ -59,11 +62,6 @@ struct TimelineRendererView: View {
.sectionHeader {
header(withModel: model, for: group, calendarHelper: model.calendarHelper)
}
.sectionFooter {
VStack {
Divider()
}
}
.selfSizingConfig { (_) -> ASSelfSizingConfig? in
.init(
selfSizeHorizontally: false,
......@@ -77,15 +75,15 @@ struct TimelineRendererView: View {
@ViewBuilder
func renderElement(_ element: TimelineElement) -> some View {
if element.isGroup {
TimelineItemView(icon: Image(systemName: "rectangle.stack"),
title: "\(element.items.count) \(element.itemType.titleCase())\(element.items.count != 1 ? "s" : "")",
backgroundColor: .gray)
.frame(maxWidth: .infinity, alignment: .leading)
}
else if let item = element.items.first {
self.viewContext.render(item: item)
.frame(maxWidth: .infinity, alignment: .leading)
if grouped {
GroupedTimelineItemView(items: element.items)
} else {
VStack {
ForEach(element.items) { item in
self.viewContext.render(item: item)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
......@@ -104,7 +102,7 @@ struct TimelineRendererView: View {
ASCollectionLayout(scrollDirection: .vertical,
interSectionSpacing: 0) { () -> ASCollectionLayoutSection in
ASCollectionLayoutSection { _ -> NSCollectionLayoutSection in
let hasFullWidthHeader: Bool = model.detailLevel == .year
let hasFullWidthHeader: Bool = grouped
let itemLayoutSize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
......@@ -125,12 +123,12 @@ struct TimelineRendererView: View {
let section = NSCollectionLayoutSection(group: group)
section.contentInsets = .init(
top: 8,
top: 20,
leading: hasFullWidthHeader ? 10 : self.leadingInset + 5,
bottom: 8,
bottom: 20,
trailing: 10
)
section.interGroupSpacing = 10
section.interGroupSpacing = 20
section.visibleItemsInvalidationHandler = { _, _, _ in
// If this isn't defined, there is a bug in UICVCompositional Layout that will fail to update sizes of cells
}
......@@ -139,7 +137,7 @@ struct TimelineRendererView: View {
if hasFullWidthHeader {
let supplementarySize = NSCollectionLayoutSize(
widthDimension: .fractionalWidth(1.0),
heightDimension: .absolute(35)
heightDimension: .absolute(50)
)
headerSupplementary = NSCollectionLayoutBoundarySupplementaryItem(
layoutSize: supplementarySize,
......@@ -183,120 +181,155 @@ struct TimelineRendererView: View {
extension TimelineRendererView {
// TODO: Clean up this function. Should probably define for each `DetailLevel` individually
func header(withModel model: TimelineRendererModel, for group: TimelineGroup, calendarHelper: CalendarHelper) -> some View {
let matchesNow = calendarHelper.isSameAsNow(
group.date,
byComponents: model.detailLevel.relevantComponents
)
let flipOrder: Bool = {
switch model.detailLevel {
case .hour: return true
default:
return false
}
}()
let alignment: HorizontalAlignment = {
switch model.detailLevel {
case .year: return .leading
case .day: return .center
default: return .trailing
}
}()
let largeString: String? = {
guard showTimeAgo == false else {
return group.date.timestampString ?? ""
}
if grouped {
let format = DateFormatter()
format.dateFormat = "MMMM YYYY"
switch model.detailLevel {
case .hour:
if group.isStartOf.contains(.day) {
return
AnyView (
HStack(alignment: .center) {
VStack(alignment: .leading) {
Text(format.string(from: group.date))
.font(Font.system(size: 20).bold())
.foregroundColor(.black)
.lineLimit(1)
.minimumScaleFactor(0.6)
.fixedSize()
Text(getCount(data: group))
.foregroundColor(.gray)
.lineLimit(1)
.minimumScaleFactor(0.6)
.fixedSize()
Divider()
}
Spacer(minLength: 0)
}
.padding(.leading, 20)
.padding(.top, 20)
.frame(maxWidth: .infinity, alignment: Alignment(horizontal: .leading, vertical: .center))
)
} else {
let matchesNow = calendarHelper.isSameAsNow(
group.date,
byComponents: model.detailLevel.relevantComponents
)
let flipOrder: Bool = {
switch model.detailLevel {
case .hour: return true
default:
return false
}
}()
let alignment: HorizontalAlignment = {
if grouped {
return . leading
}
switch model.detailLevel {
case .year: return .leading
case .day: return .center
default: return .trailing
}
}()
let largeString: String? = {
guard showTimeAgo == false else {
return group.date.timestampString ?? ""
}
switch model.detailLevel {
case .hour:
if group.isStartOf.contains(.day) {
let format = DateFormatter()
format.dateFormat = "dd/MM"
return format.string(from: group.date)
}
case .day:
let format = DateFormatter()
format.dateFormat = "dd/MM"
format.dateFormat = "d"
return format.string(from: group.date)
}
case .day:
let format = DateFormatter()
format.dateFormat = "d"
return format.string(from: group.date)
case .week:
let format = DateFormatter()
format.dateFormat = "ww"
return format.string(from: group.date)
case .month:
let format = DateFormatter()
format.dateFormat = "MMM"
return format.string(from: group.date)
case .year:
let format = DateFormatter()
format.dateFormat = "YYYY"
return format.string(from: group.date)
}
return nil
}()
let smallString: String? = {
guard showTimeAgo == false else {
return group.date.timestampString ?? ""
}
switch model.detailLevel {
case .hour:
let format = DateFormatter()
format.dateFormat = "h a"
return format.string(from: group.date)
case .day:
let format = DateFormatter()
format.dateFormat = "MMM" // group.isStartOf.contains(.year) ? "MMM YY" : "MMM"
return format.string(from: group.date)
case .week:
return "Week"
case .month:
if group.isStartOf.contains(.year) {
case .week:
let format = DateFormatter()
format.dateFormat = "ww"
return format.string(from: group.date)
case .month:
let format = DateFormatter()
format.dateFormat = "MMM"
return format.string(from: group.date)
case .year:
let format = DateFormatter()
format.dateFormat = "YYYY"
return format.string(from: group.date)
}
default: break
}
return nil
}()
let small: some View = {
smallString.map { string in
Text(string)
.font(Font.system(size: 14))
.foregroundColor(matchesNow ? Color.red : Color(.secondaryLabel))
.fixedSize()
}
}()
return VStack(alignment: alignment, spacing: 0) {
if !flipOrder {
small
}
largeString.map { string in
Text(string)
.font(Font.system(size: 20))
.lineLimit(1)
.minimumScaleFactor(0.6)
.foregroundColor(matchesNow ? (useFillToIndicateNow(model: model) ? Color.white : .red) :
Color(.label))
.padding(.vertical, matchesNow ? 3 : 0)
.background(
Circle().fill((useFillToIndicateNow(model: model) && matchesNow) ? Color.red : .clear)
.frame(minWidth: 30, minHeight: 30)
)
.fixedSize()
}
if flipOrder {
small
}
Spacer(minLength: 0)
return nil
}()
let smallString: String? = {
guard showTimeAgo == false else {
return group.date.timestampString ?? ""
}
switch model.detailLevel {
case .hour:
let format = DateFormatter()
format.dateFormat = "h a"
return format.string(from: group.date)
case .day:
let format = DateFormatter()
format.dateFormat = "MMM" // group.isStartOf.contains(.year) ? "MMM YY" : "MMM"
return format.string(from: group.date)
case .week:
return "Week"
case .month:
if group.isStartOf.contains(.year) {
let format = DateFormatter()
format.dateFormat = "YYYY"
return format.string(from: group.date)
}
default: break
}
return nil
}()
let small: some View = {
smallString.map { string in
Text(string)
.font(Font.system(size: 14))
.foregroundColor(matchesNow ? Color.red : Color(.secondaryLabel))
.fixedSize()
}
}()
return
AnyView(
VStack(alignment: alignment, spacing: 0) {
if !flipOrder {
small
}
largeString.map { string in
Text(string)
.font(Font.system(size: 20))
.lineLimit(1)
.minimumScaleFactor(0.6)
.foregroundColor(matchesNow ? (useFillToIndicateNow(model: model) ? Color.white : .red) :
Color(.label))
.padding(.vertical, matchesNow ? 3 : 0)
.background(
Circle().fill((useFillToIndicateNow(model: model) && matchesNow) ? Color.red : .clear)
.frame(minWidth: 30, minHeight: 30)
)
.fixedSize()
}
if flipOrder {
small
}
Spacer(minLength: 0)
}
.padding(8)
.frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .top))
)
}
.padding(8)
.frame(maxWidth: .infinity, alignment: Alignment(horizontal: alignment, vertical: .top))
}
func useFillToIndicateNow(model: TimelineRendererModel) -> Bool {
......@@ -307,4 +340,27 @@ extension TimelineRendererView {
return false
}
}
func getCount(data: TimelineGroup) -> String {
var count: String = ""
var messageCount = 0
var emailCount = 0
var photoCount = 0
_ =
data.items.map { item in
messageCount = messageCount + item.items.filter { $0.type == "Message" }.count
emailCount = emailCount + item.items.filter { $0.type == "EmailMessage" }.count
photoCount = photoCount + item.items.filter { $0.type == "Photo" }.count
}
if emailCount > 0 {
count = "\(emailCount) emails "
}
if messageCount > 0 {
count = count + "\(messageCount) conversations "
}
if photoCount > 0 {
count = count + "\(photoCount) photos"
}
return count
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment