mirror of https://github.com/go-gitea/gitea.git
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:
parent
f8f394cb0e
commit
cdc33b29a0
|
@ -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
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue