Add global setting how timestamps should be rendered (#28657)

- 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>
This commit is contained in:
Yarden Shoham 2024-01-02 03:25:30 +02:00 committed by GitHub
parent f8f394cb0e
commit cdc33b29a0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 69 additions and 49 deletions

View file

@ -1244,6 +1244,10 @@ LEVEL = Info
;; Change the sort type of the explore pages. ;; Change the sort type of the explore pages.
;; Default is "recentupdate", but you also have "alphabetically", "reverselastlogin", "newest", "oldest". ;; Default is "recentupdate", but you also have "alphabetically", "reverselastlogin", "newest", "oldest".
;EXPLORE_PAGING_DEFAULT_SORT = recentupdate ;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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View file

@ -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. - `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). 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" - `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`) ### UI - Admin (`ui.admin`)

View file

@ -7,33 +7,35 @@ import (
"time" "time"
"code.gitea.io/gitea/modules/container" "code.gitea.io/gitea/modules/container"
"code.gitea.io/gitea/modules/log"
) )
// UI settings // UI settings
var UI = struct { var UI = struct {
ExplorePagingNum int ExplorePagingNum int
SitemapPagingNum int SitemapPagingNum int
IssuePagingNum int IssuePagingNum int
RepoSearchPagingNum int RepoSearchPagingNum int
MembersPagingNum int MembersPagingNum int
FeedMaxCommitNum int FeedMaxCommitNum int
FeedPagingNum int FeedPagingNum int
PackagesPagingNum int PackagesPagingNum int
GraphMaxCommitNum int GraphMaxCommitNum int
CodeCommentLines int CodeCommentLines int
ReactionMaxUserNum int ReactionMaxUserNum int
MaxDisplayFileSize int64 MaxDisplayFileSize int64
ShowUserEmail bool ShowUserEmail bool
DefaultShowFullName bool DefaultShowFullName bool
DefaultTheme string DefaultTheme string
Themes []string Themes []string
Reactions []string Reactions []string
ReactionsLookup container.Set[string] `ini:"-"` ReactionsLookup container.Set[string] `ini:"-"`
CustomEmojis []string CustomEmojis []string
CustomEmojisMap map[string]string `ini:"-"` CustomEmojisMap map[string]string `ini:"-"`
SearchRepoDescription bool SearchRepoDescription bool
OnlyShowRelevantRepos bool OnlyShowRelevantRepos bool
ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"` ExploreDefaultSort string `ini:"EXPLORE_PAGING_DEFAULT_SORT"`
PreferredTimestampTense string
AmbiguousUnicodeDetection bool AmbiguousUnicodeDetection bool
@ -67,23 +69,24 @@ var UI = struct {
Keywords string Keywords string
} `ini:"ui.meta"` } `ini:"ui.meta"`
}{ }{
ExplorePagingNum: 20, ExplorePagingNum: 20,
SitemapPagingNum: 20, SitemapPagingNum: 20,
IssuePagingNum: 20, IssuePagingNum: 20,
RepoSearchPagingNum: 20, RepoSearchPagingNum: 20,
MembersPagingNum: 20, MembersPagingNum: 20,
FeedMaxCommitNum: 5, FeedMaxCommitNum: 5,
FeedPagingNum: 20, FeedPagingNum: 20,
PackagesPagingNum: 20, PackagesPagingNum: 20,
GraphMaxCommitNum: 100, GraphMaxCommitNum: 100,
CodeCommentLines: 4, CodeCommentLines: 4,
ReactionMaxUserNum: 10, ReactionMaxUserNum: 10,
MaxDisplayFileSize: 8388608, MaxDisplayFileSize: 8388608,
DefaultTheme: `gitea-auto`, DefaultTheme: `gitea-auto`,
Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`}, Themes: []string{`gitea-auto`, `gitea-light`, `gitea-dark`},
Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`}, Reactions: []string{`+1`, `-1`, `laugh`, `hooray`, `confused`, `heart`, `rocket`, `eyes`},
CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`}, CustomEmojis: []string{`git`, `gitea`, `codeberg`, `gitlab`, `github`, `gogs`},
CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"}, CustomEmojisMap: map[string]string{"git": ":git:", "gitea": ":gitea:", "codeberg": ":codeberg:", "gitlab": ":gitlab:", "github": ":github:", "gogs": ":gogs:"},
PreferredTimestampTense: "mixed",
AmbiguousUnicodeDetection: true, AmbiguousUnicodeDetection: true,
@ -142,6 +145,10 @@ func loadUIFrom(rootCfg ConfigProvider) {
UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false) UI.DefaultShowFullName = sec.Key("DEFAULT_SHOW_FULL_NAME").MustBool(false)
UI.SearchRepoDescription = sec.Key("SEARCH_REPO_DESCRIPTION").MustBool(true) 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, // 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. // 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) UI.OnlyShowRelevantRepos = sec.Key("ONLY_SHOW_RELEVANT_REPOS").MustBool(false)

View file

@ -7,11 +7,12 @@ import (
"fmt" "fmt"
"html" "html"
"html/template" "html/template"
"strings"
"time" "time"
) )
// DateTime renders an absolute time HTML element by datetime. // 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 { if p, ok := datetime.(*time.Time); ok {
datetime = *p datetime = *p
} }
@ -48,13 +49,15 @@ func DateTime(format string, datetime any) template.HTML {
panic(fmt.Sprintf("Unsupported time type %T", datetime)) panic(fmt.Sprintf("Unsupported time type %T", datetime))
} }
extraAttrs := strings.Join(attrs, " ")
switch format { switch format {
case "short": 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": 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": 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)) panic(fmt.Sprintf("Unsupported format %s", format))
} }

View file

@ -29,17 +29,17 @@ func TestDateTime(t *testing.T) {
assert.EqualValues(t, "-", DateTime("short", TimeStamp(0))) assert.EqualValues(t, "-", DateTime("short", TimeStamp(0)))
actual := DateTime("short", "invalid") 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) 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) 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) 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) 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)
} }

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"time" "time"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/translation" "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 // TimeSince renders relative time HTML given a time.Time
func TimeSince(then time.Time, lang translation.Locale) template.HTML { 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) return timeSinceUnix(then, time.Now(), lang)
} }

View file

@ -247,6 +247,7 @@ export default {
<div class="gt-ellipsis text light-2"> <div class="gt-ellipsis text light-2">
{{ commit.committer_or_author_name }} {{ commit.committer_or_author_name }}
<span class="text right"> <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> <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
</span> </span>
</div> </div>