Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
memri
iOS client for Memri
Commits
f78c117b
Commit
f78c117b
authored
Aug 15, 2021
by
Chaitanya Pandit
Committed by
Azat Alimov
Aug 15, 2021
Browse files
Resolved: Grouped timeline (without time)
parent
8490b893
Changes
3
Hide whitespace changes
Inline
Side-by-side
MemriApp/Assets/defaultCVU/type/Person.cvu
View file @
f78c117b
...
...
@@ -382,6 +382,8 @@ Person {
[renderer = timeline] {
timeProperty: dateCreated
detailLevel: month
grouped: true
}
}
...
...
MemriApp/UI/CVUComponents/CVUElements/CVU_TimelineItem.swift
View file @
f78c117b
...
...
@@ -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
:
[])
}
}
MemriApp/UI/Renderers/TimelineRenderer.swift
View file @
f78c117b
...
...
@@ -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
.
isG
roup
{
TimelineItemView
(
i
con
:
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
g
roup
ed
{
Grouped
TimelineItemView
(
i
tems
:
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
=
1
0
section
.
interGroupSpacing
=
2
0
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
(
3
5
)
heightDimension
:
.
absolute
(
5
0
)
)
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
=
"d
d/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
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment