- Resolves https://github.com/go-gitea/gitea/issues/22493 - Related to https://github.com/go-gitea/gitea/issues/4520 Some admins prefer all timestamps to display the full date instead of relative time. They can do that now by setting ```ini [ui] PREFERRED_TIMESTAMP_TENSE = absolute ``` This setting is set to `mixed` by default, allowing dates to render as "5 hours ago". Here are some screenshots of the UI with this setting set to `absolute`: ![image](https://github.com/go-gitea/gitea/assets/20454870/f496457f-6afa-44be-a1e7-249ee5fe0706) ![image](https://github.com/go-gitea/gitea/assets/20454870/c03b14f5-063d-4e13-9780-76ab002d76a9) ![image](https://github.com/go-gitea/gitea/assets/20454870/f4b34e28-1546-4374-9199-c43348844edd) --------- Signed-off-by: Yarden Shoham <git@yardenshoham.com> Co-authored-by: delvh <dev.lh@web.de>tags/v1.22.0-rc0
@@ -1244,6 +1244,10 @@ LEVEL = Info | |||
;; Change the sort type of the explore pages. | |||
;; Default is "recentupdate", but you also have "alphabetically", "reverselastlogin", "newest", "oldest". | |||
;EXPLORE_PAGING_DEFAULT_SORT = recentupdate | |||
;; | |||
;; The tense all timestamps should be rendered in. Possible values are `absolute` time (i.e. 1970-01-01, 11:59) and `mixed`. | |||
;; `mixed` means most timestamps are rendered in relative time (i.e. 2 days ago). | |||
;PREFERRED_TIMESTAMP_TENSE = mixed | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | |||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; |
@@ -231,6 +231,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a | |||
- `ONLY_SHOW_RELEVANT_REPOS`: **false**: Whether to only show relevant repos on the explore page when no keyword is specified and default sorting is used. | |||
A repo is considered irrelevant if it's a fork or if it has no metadata (no description, no icon, no topic). | |||
- `EXPLORE_PAGING_DEFAULT_SORT`: **recentupdate**: Change the sort type of the explore pages. Valid values are "recentupdate", "alphabetically", "reverselastlogin", "newest" and "oldest" | |||
- `PREFERRED_TIMESTAMP_TENSE`: **mixed**: The tense all timestamps should be rendered in. Possible values are `absolute` time (i.e. 1970-01-01, 11:59) and `mixed`. `mixed` means most timestamps are rendered in relative time (i.e. 2 days ago). | |||
### UI - Admin (`ui.admin`) | |||
@@ -7,33 +7,35 @@ import ( | |||
"time" | |||
"code.gitea.io/gitea/modules/container" | |||
"code.gitea.io/gitea/modules/log" | |||
) | |||
// UI settings | |||
var UI = struct { | |||
ExplorePagingNum int | |||
SitemapPagingNum int | |||
IssuePagingNum int | |||
RepoSearchPagingNum int | |||
MembersPagingNum int | |||
FeedMaxCommitNum int | |||
FeedPagingNum int | |||
PackagesPagingNum int | |||
GraphMaxCommitNum int | |||
CodeCommentLines int | |||
ReactionMaxUserNum int | |||
MaxDisplayFileSize int64 | |||
ShowUserEmail bool | |||
DefaultShowFullName bool | |||
DefaultTheme string | |||
Themes []string | |||
Reactions []string | |||
ReactionsLookup container.Set[string] `ini:"-"` | |||
CustomEmojis []string | |||
CustomEmojisMap map[string]string `ini:"-"` | |||
SearchRepoDescription bool | |||
OnlyShowRelevantRepos bool | |||
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"` | |||
ExplorePagingNum int | |||
SitemapPagingNum int | |||
IssuePagingNum int | |||
RepoSearchPagingNum int | |||
MembersPagingNum int | |||
FeedMaxCommitNum int | |||
FeedPagingNum int | |||
PackagesPagingNum int | |||
GraphMaxCommitNum int | |||
CodeCommentLines int | |||
ReactionMaxUserNum int | |||
MaxDisplayFileSize int64 | |||
ShowUserEmail bool | |||
DefaultShowFullName bool | |||
DefaultTheme string | |||
Themes []string | |||
Reactions []string | |||
ReactionsLookup container.Set[string] `ini:"-"` | |||
CustomEmojis []string | |||
CustomEmojisMap map[string]string `ini:"-"` | |||
SearchRepoDescription bool | |||
OnlyShowRelevantRepos bool | |||
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"` | |||
PreferredTimestampTense string | |||
AmbiguousUnicodeDetection bool | |||
@@ -67,23 +69,24 @@ var UI = struct { | |||
Keywords string | |||
} `ini:"ui.meta"` | |||
}{ | |||
ExplorePagingNum: 20, | |||
SitemapPagingNum: 20, | |||
IssuePagingNum: 20, | |||
RepoSearchPagingNum: 20, | |||
MembersPagingNum: 20, | |||
FeedMaxCommitNum: 5, | |||
FeedPagingNum: 20, | |||
PackagesPagingNum: 20, | |||
GraphMaxCommitNum: 100, | |||
CodeCommentLines: 4, | |||
ReactionMaxUserNum: 10, | |||
MaxDisplayFileSize: 8388608, | |||
DefaultTheme: `gitea-auto`, | |||
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`}, | |||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | |||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, | |||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, | |||
ExplorePagingNum: 20, | |||
SitemapPagingNum: 20, | |||
IssuePagingNum: 20, | |||
RepoSearchPagingNum: 20, | |||
MembersPagingNum: 20, | |||
FeedMaxCommitNum: 5, | |||
FeedPagingNum: 20, | |||
PackagesPagingNum: 20, | |||
GraphMaxCommitNum: 100, | |||
CodeCommentLines: 4, | |||
ReactionMaxUserNum: 10, | |||
MaxDisplayFileSize: 8388608, | |||
DefaultTheme: `gitea-auto`, | |||
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`}, | |||
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, | |||
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, | |||
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, | |||
PreferredTimestampTense: "mixed", | |||
AmbiguousUnicodeDetection: true, | |||
@@ -142,6 +145,10 @@ func loadUIFrom(rootCfg ConfigProvider) { | |||
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false) | |||
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true) | |||
if UI.PreferredTimestampTense != "mixed" && UI.PreferredTimestampTense != "absolute" { | |||
log.Fatal("ui.PREFERRED_TIMESTAMP_TENSE must be either 'mixed' or 'absolute'") | |||
} | |||
// OnlyShowRelevantRepos=false is important for many private/enterprise instances, | |||
// because many private repositories do not have "description/topic", users just want to search by their names. | |||
UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false) |
@@ -7,11 +7,12 @@ import ( | |||
"fmt" | |||
"html" | |||
"html/template" | |||
"strings" | |||
"time" | |||
) | |||
// DateTime renders an absolute time HTML element by datetime. | |||
func DateTime(format string, datetime any) template.HTML { | |||
func DateTime(format string, datetime any, attrs ...string) template.HTML { | |||
if p, ok := datetime.(*time.Time); ok { | |||
datetime = *p | |||
} | |||
@@ -48,13 +49,15 @@ func DateTime(format string, datetime any) template.HTML { | |||
panic(fmt.Sprintf("Unsupported time type %T", datetime)) | |||
} | |||
extraAttrs := strings.Join(attrs, " ") | |||
switch format { | |||
case "short": | |||
return template.HTML(fmt.Sprintf(`<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="%s">%s</relative-time>`, datetimeEscaped, textEscaped)) | |||
return template.HTML(fmt.Sprintf(`<relative-time %s format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="%s">%s</relative-time>`, extraAttrs, datetimeEscaped, textEscaped)) | |||
case "long": | |||
return template.HTML(fmt.Sprintf(`<relative-time format="datetime" year="numeric" month="long" day="numeric" weekday="" datetime="%s">%s</relative-time>`, datetimeEscaped, textEscaped)) | |||
return template.HTML(fmt.Sprintf(`<relative-time %s format="datetime" year="numeric" month="long" day="numeric" weekday="" datetime="%s">%s</relative-time>`, extraAttrs, datetimeEscaped, textEscaped)) | |||
case "full": | |||
return template.HTML(fmt.Sprintf(`<relative-time format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="%s">%s</relative-time>`, datetimeEscaped, textEscaped)) | |||
return template.HTML(fmt.Sprintf(`<relative-time %s format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="%s">%s</relative-time>`, extraAttrs, datetimeEscaped, textEscaped)) | |||
} | |||
panic(fmt.Sprintf("Unsupported format %s", format)) | |||
} |
@@ -29,17 +29,17 @@ func TestDateTime(t *testing.T) { | |||
assert.EqualValues(t, "-", DateTime("short", TimeStamp(0))) | |||
actual := DateTime("short", "invalid") | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="invalid">invalid</relative-time>`, actual) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="invalid">invalid</relative-time>`, actual) | |||
actual = DateTime("short", refTimeStr) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2018-01-01T00:00:00Z">2018-01-01T00:00:00Z</relative-time>`, actual) | |||
actual = DateTime("short", refTime) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2018-01-01T00:00:00Z">2018-01-01</relative-time>`, actual) | |||
actual = DateTime("short", refTimeStamp) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual) | |||
assert.EqualValues(t, `<relative-time format="datetime" year="numeric" month="short" day="numeric" weekday="" datetime="2017-12-31T19:00:00-05:00">2017-12-31</relative-time>`, actual) | |||
actual = DateTime("full", refTimeStamp) | |||
assert.EqualValues(t, `<relative-time format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual) | |||
assert.EqualValues(t, `<relative-time format="datetime" weekday="" year="numeric" month="short" day="numeric" hour="numeric" minute="numeric" second="numeric" datetime="2017-12-31T19:00:00-05:00">2017-12-31 19:00:00 -05:00</relative-time>`, actual) | |||
} |
@@ -9,6 +9,7 @@ import ( | |||
"strings" | |||
"time" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/translation" | |||
) | |||
@@ -132,6 +133,9 @@ func timeSinceUnix(then, now time.Time, lang translation.Locale) template.HTML { | |||
// TimeSince renders relative time HTML given a time.Time | |||
func TimeSince(then time.Time, lang translation.Locale) template.HTML { | |||
if setting.UI.PreferredTimestampTense == "absolute" { | |||
return DateTime("full", then, `class="time-since"`) | |||
} | |||
return timeSinceUnix(then, time.Now(), lang) | |||
} | |||
@@ -247,6 +247,7 @@ export default { | |||
<div class="gt-ellipsis text light-2"> | |||
{{ commit.committer_or_author_name }} | |||
<span class="text right"> | |||
<!-- TODO: make this respect the PreferredTimestampTense setting --> | |||
<relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time> | |||
</span> | |||
</div> |