44package templates
55
66import (
7- "context"
7+ "fmt"
8+ "html"
89 "html/template"
10+ "strings"
911 "time"
1012
1113 "code.gitea.io/gitea/modules/setting"
1214 "code.gitea.io/gitea/modules/timeutil"
15+ "code.gitea.io/gitea/modules/translation"
1316)
1417
15- type DateUtils struct {
16- ctx context.Context
17- }
18+ type DateUtils struct {}
1819
19- func NewDateUtils (ctx context. Context ) * DateUtils {
20- return & DateUtils { ctx }
20+ func NewDateUtils () * DateUtils {
21+ return ( * DateUtils )( nil ) // the util is stateless, and we do not need to create an instance
2122}
2223
2324// AbsoluteShort renders in "Jan 01, 2006" format
2425func (du * DateUtils ) AbsoluteShort (time any ) template.HTML {
25- return timeutil . DateTime ("short" , time )
26+ return dateTimeFormat ("short" , time )
2627}
2728
2829// AbsoluteLong renders in "January 01, 2006" format
2930func (du * DateUtils ) AbsoluteLong (time any ) template.HTML {
30- return timeutil . DateTime ("short" , time )
31+ return dateTimeFormat ("short" , time )
3132}
3233
3334// FullTime renders in "Jan 01, 2006 20:33:44" format
3435func (du * DateUtils ) FullTime (time any ) template.HTML {
35- return timeutil .DateTime ("full" , time )
36+ return dateTimeFormat ("full" , time )
37+ }
38+
39+ func (du * DateUtils ) TimeSince (time any ) template.HTML {
40+ return TimeSince (time )
3641}
3742
3843// ParseLegacy parses the datetime in legacy format, eg: "2016-01-02" in server's timezone.
@@ -56,5 +61,91 @@ func dateTimeLegacy(format string, datetime any, _ ...string) template.HTML {
5661 if s , ok := datetime .(string ); ok {
5762 datetime = parseLegacy (s )
5863 }
59- return timeutil .DateTime (format , datetime )
64+ return dateTimeFormat (format , datetime )
65+ }
66+
67+ func timeSinceLegacy (time any , _ translation.Locale ) template.HTML {
68+ if ! setting .IsProd || setting .IsInTesting {
69+ panic ("timeSinceLegacy is for backward compatibility only, do not use it in new code" )
70+ }
71+ return TimeSince (time )
72+ }
73+
74+ func anyToTime (any any ) (t time.Time , isZero bool ) {
75+ switch v := any .(type ) {
76+ case nil :
77+ // it is zero
78+ case * time.Time :
79+ if v != nil {
80+ t = * v
81+ }
82+ case time.Time :
83+ t = v
84+ case timeutil.TimeStamp :
85+ t = v .AsTime ()
86+ case timeutil.TimeStampNano :
87+ t = v .AsTime ()
88+ case int :
89+ t = timeutil .TimeStamp (v ).AsTime ()
90+ case int64 :
91+ t = timeutil .TimeStamp (v ).AsTime ()
92+ default :
93+ panic (fmt .Sprintf ("Unsupported time type %T" , any ))
94+ }
95+ return t , t .IsZero () || t .Unix () == 0
96+ }
97+
98+ func dateTimeFormat (format string , datetime any ) template.HTML {
99+ t , isZero := anyToTime (datetime )
100+ if isZero {
101+ return "-"
102+ }
103+ var textEscaped string
104+ datetimeEscaped := html .EscapeString (t .Format (time .RFC3339 ))
105+ if format == "full" {
106+ textEscaped = html .EscapeString (t .Format ("2006-01-02 15:04:05 -07:00" ))
107+ } else {
108+ textEscaped = html .EscapeString (t .Format ("2006-01-02" ))
109+ }
110+
111+ attrs := []string {`weekday=""` , `year="numeric"` }
112+ switch format {
113+ case "short" , "long" : // date only
114+ attrs = append (attrs , `month="` + format + `"` , `day="numeric"` )
115+ return template .HTML (fmt .Sprintf (`<absolute-date %s date="%s">%s</absolute-date>` , strings .Join (attrs , " " ), datetimeEscaped , textEscaped ))
116+ case "full" : // full date including time
117+ attrs = append (attrs , `format="datetime"` , `month="short"` , `day="numeric"` , `hour="numeric"` , `minute="numeric"` , `second="numeric"` , `data-tooltip-content` , `data-tooltip-interactive="true"` )
118+ return template .HTML (fmt .Sprintf (`<relative-time %s datetime="%s">%s</relative-time>` , strings .Join (attrs , " " ), datetimeEscaped , textEscaped ))
119+ default :
120+ panic (fmt .Sprintf ("Unsupported format %s" , format ))
121+ }
122+ }
123+
124+ func timeSinceTo (then any , now time.Time ) template.HTML {
125+ thenTime , isZero := anyToTime (then )
126+ if isZero {
127+ return "-"
128+ }
129+
130+ friendlyText := thenTime .Format ("2006-01-02 15:04:05 -07:00" )
131+
132+ // document: https://github.com/github/relative-time-element
133+ attrs := `tense="past"`
134+ isFuture := now .Before (thenTime )
135+ if isFuture {
136+ attrs = `tense="future"`
137+ }
138+
139+ // declare data-tooltip-content attribute to switch from "title" tooltip to "tippy" tooltip
140+ htm := fmt .Sprintf (`<relative-time prefix="" %s datetime="%s" data-tooltip-content data-tooltip-interactive="true">%s</relative-time>` ,
141+ attrs , thenTime .Format (time .RFC3339 ), friendlyText )
142+ return template .HTML (htm )
143+ }
144+
145+ // TimeSince renders relative time HTML given a time
146+ func TimeSince (then any ) template.HTML {
147+ if setting .UI .PreferredTimestampTense == "absolute" {
148+ return dateTimeFormat ("full" , then )
149+ }
150+ return timeSinceTo (then , time .Now ())
60151}
0 commit comments