Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce the "Prefer absolute timestamps" setting for users that want to disable relative time #24342

Closed
wants to merge 12 commits into from
2 changes: 2 additions & 0 deletions models/user/setting_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ const (
UserActivityPubPrivPem = "activitypub.priv_pem"
// UserActivityPubPubPem is user's public key
UserActivityPubPubPem = "activitypub.pub_pem"
// SettingsPreferAbsoluteTimestamps is the setting key for hidden comment types
SettingsPreferAbsoluteTimestamps = "timestamps.prefer_absolute"
)
11 changes: 7 additions & 4 deletions modules/timeutil/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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))
}
32 changes: 29 additions & 3 deletions modules/timeutil/since.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,30 @@
package timeutil

import (
"context"
"fmt"
"html/template"
"strconv"
"strings"
"time"

"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/translation"
)

type PreferenceHelper struct {
GetSetting func(ctx context.Context, key string, def ...string) (string, error)
SettingsPreferAbsoluteTimestamps string
}

var preferenceHelper PreferenceHelper

func Init(ph *PreferenceHelper) {
if ph != nil {
preferenceHelper = *ph
}
}

// Seconds-based time units
const (
Minute = 60
Expand Down Expand Up @@ -131,11 +147,21 @@ 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 {
func TimeSince(ctx context.Context, then time.Time, lang translation.Locale) template.HTML {
// if user prefers absolute timestamps, use the full time
val, err := preferenceHelper.GetSetting(ctx, preferenceHelper.SettingsPreferAbsoluteTimestamps, "false")
if err != nil {
log.Error("GetSetting %w", err)
}
preferAbsoluteTimestamps, _ := strconv.ParseBool(val) // we can safely ignore the failed conversion here
if preferAbsoluteTimestamps {
return DateTime("full", then, `class="time-since"`)
}

return timeSinceUnix(then, time.Now(), lang)
}

// TimeSinceUnix renders relative time HTML given a TimeStamp
func TimeSinceUnix(then TimeStamp, lang translation.Locale) template.HTML {
return TimeSince(then.AsLocalTime(), lang)
func TimeSinceUnix(ctx context.Context, then TimeStamp, lang translation.Locale) template.HTML {
return TimeSince(ctx, then.AsLocalTime(), lang)
}
3 changes: 3 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,9 @@ saved_successfully = Your settings were saved successfully.
privacy = Privacy
keep_activity_private = Hide the activity from the profile page
keep_activity_private_popup = Makes the activity visible only for you and the admins
timestamps = Timestamps
prefer_absolute_timestamps = Prefer absolute timestamps
prefer_absolute_timestamps_popup = Display timestamps as absolute dates instead of relative time

lookup_avatar_by_mail = Look Up Avatar by Email Address
federated_avatar_lookup = Federated Avatar Lookup
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/blame.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
commitCnt++

// User avatar image
commitSince := timeutil.TimeSinceUnix(timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale)
commitSince := timeutil.TimeSinceUnix(ctx, timeutil.TimeStamp(commit.Author.When.Unix()), ctx.Locale)

var avatar string
if commit.User != nil {
Expand Down
2 changes: 1 addition & 1 deletion routers/web/repo/issue_content_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func GetContentHistoryList(ctx *context.Context) {
class := avatars.DefaultAvatarClass + " gt-mr-3"
name := html.EscapeString(username)
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
timeSinceText := string(timeutil.TimeSinceUnix(ctx, item.EditedUnix, ctx.Locale))

results = append(results, map[string]interface{}{
"name": avatarHTML + "<strong>" + name + "</strong> " + actionText + " " + timeSinceText,
Expand Down
22 changes: 22 additions & 0 deletions routers/web/user/setting/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http"
"os"
"path/filepath"
"strconv"
"strings"

"code.gitea.io/gitea/models/db"
Expand Down Expand Up @@ -350,6 +351,14 @@ func Appearance(ctx *context.Context) {
return forms.IsUserHiddenCommentTypeGroupChecked(commentTypeGroup, hiddenCommentTypes)
}

val, err = user_model.GetUserSetting(ctx.Doer.ID, user_model.SettingsPreferAbsoluteTimestamps, "false")
if err != nil {
ctx.ServerError("GetUserSetting", err)
return
}
preferAbsoluteTimestamps, _ := strconv.ParseBool(val) // we can safely ignore the failed conversion here
ctx.Data["PreferAbsoluteTimestamps"] = preferAbsoluteTimestamps

ctx.HTML(http.StatusOK, tplSettingsAppearance)
}

Expand Down Expand Up @@ -421,3 +430,16 @@ func UpdateUserHiddenComments(ctx *context.Context) {
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}

// UpdateUserTimestamps update a user's timestamp preferences
func UpdateUserTimestamps(ctx *context.Context) {
err := user_model.SetUserSetting(ctx.Doer.ID, user_model.SettingsPreferAbsoluteTimestamps, strconv.FormatBool(forms.UserTimestampsFromRequest(ctx).PreferAbsoluteTimestamps))
if err != nil {
ctx.ServerError("SetUserSetting", err)
return
}

log.Trace("User settings updated: %s", ctx.Doer.Name)
ctx.Flash.Success(ctx.Tr("settings.saved_successfully"))
ctx.Redirect(setting.AppSubURL + "/user/settings/appearance")
}
3 changes: 3 additions & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
context_service "code.gitea.io/gitea/services/context"
"code.gitea.io/gitea/services/forms"
"code.gitea.io/gitea/services/lfs"
user_service "code.gitea.io/gitea/services/user"

_ "code.gitea.io/gitea/modules/session" // to registers all internal adapters

Expand Down Expand Up @@ -397,6 +398,7 @@ func registerRoutes(m *web.Route) {
m.Get("/login/oauth/keys", ignSignInAndCsrf, auth.OIDCKeys)
m.Post("/login/oauth/introspect", CorsHandler(), web.Bind(forms.IntrospectTokenForm{}), ignSignInAndCsrf, auth.IntrospectOAuth)

user_service.Init()
m.Group("/user/settings", func() {
m.Get("", user_setting.Profile)
m.Post("", web.Bind(forms.UpdateProfileForm{}), user_setting.ProfilePost)
Expand All @@ -414,6 +416,7 @@ func registerRoutes(m *web.Route) {
m.Get("", user_setting.Appearance)
m.Post("/language", web.Bind(forms.UpdateLanguageForm{}), user_setting.UpdateUserLang)
m.Post("/hidden_comments", user_setting.UpdateUserHiddenComments)
m.Post("/timestamps", user_setting.UpdateUserTimestamps)
m.Post("/theme", web.Bind(forms.UpdateThemeForm{}), user_setting.UpdateUIThemePost)
})
m.Group("/security", func() {
Expand Down
5 changes: 5 additions & 0 deletions services/forms/user_form.go
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,11 @@ type UpdateLanguageForm struct {
Language string
}

// UpdateTimestampsForm form for updating profile
type UpdateTimestampsForm struct {
PreferAbsoluteTimestamps bool
}

// Validate validates the fields
func (f *UpdateLanguageForm) Validate(req *http.Request, errs binding.Errors) binding.Errors {
ctx := context.GetContext(req)
Expand Down
14 changes: 14 additions & 0 deletions services/forms/user_form_timestamps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package forms

import (
"code.gitea.io/gitea/modules/context"
)

// UserTimestampsFromRequest parses the form for the absolute timestamps preference
func UserTimestampsFromRequest(ctx *context.Context) *UpdateTimestampsForm {
timestampsForm := &UpdateTimestampsForm{PreferAbsoluteTimestamps: ctx.FormBool("prefer_absolute_timestamps")}
return timestampsForm
}
17 changes: 17 additions & 0 deletions services/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import (
system_model "code.gitea.io/gitea/models/system"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/avatar"
gitea_context "code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/eventsource"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/services/packages"
)
Expand Down Expand Up @@ -290,3 +292,18 @@ func DeleteAvatar(u *user_model.User) error {
}
return nil
}

func Init() {
timeutil.Init(&timeutil.PreferenceHelper{
GetSetting: func(ctx context.Context, key string, def ...string) (string, error) {
giteaCtx, ok := ctx.(*gitea_context.Context)

// this casting should always be ok but if it fails we have to provide a fallback
if !ok {
return "false", nil
}
return user_model.GetUserSetting(giteaCtx.Doer.ID, key, def...)
},
SettingsPreferAbsoluteTimestamps: user_model.SettingsPreferAbsoluteTimestamps,
})
}
2 changes: 1 addition & 1 deletion templates/admin/process-row.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="icon gt-ml-3 gt-mr-3">{{if eq .Process.Type "request"}}{{svg "octicon-globe" 16}}{{else if eq .Process.Type "system"}}{{svg "octicon-cpu" 16}}{{else}}{{svg "octicon-terminal" 16}}{{end}}</div>
<div class="content gt-f1">
<div class="header">{{.Process.Description}}</div>
<div class="description">{{TimeSince .Process.Start .root.locale}}</div>
<div class="description">{{TimeSince $.Context .Process.Start .root.locale}}</div>
</div>
<div>
{{if ne .Process.Type "system"}}
Expand Down
2 changes: 1 addition & 1 deletion templates/admin/stacktrace-row.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</div>
<div class="content gt-f1">
<div class="header">{{.Process.Description}}</div>
<div class="description">{{if ne .Process.Type "none"}}{{TimeSince .Process.Start .root.locale}}{{end}}</div>
<div class="description">{{if ne .Process.Type "none"}}{{TimeSince .root.Context .Process.Start .root.locale}}{{end}}</div>
</div>
<div>
{{if or (eq .Process.Type "request") (eq .Process.Type "normal")}}
Expand Down
14 changes: 7 additions & 7 deletions templates/devtest/gitea-ui.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@

<div>
<h1>TimeSince</h1>
<div>Now: {{TimeSince .TimeNow $.locale}}</div>
<div>5s past: {{TimeSince .TimePast5s $.locale}}</div>
<div>5s future: {{TimeSince .TimeFuture5s $.locale}}</div>
<div>2m past: {{TimeSince .TimePast2m $.locale}}</div>
<div>2m future: {{TimeSince .TimeFuture2m $.locale}}</div>
<div>1y past: {{TimeSince .TimePast1y $.locale}}</div>
<div>1y future: {{TimeSince .TimeFuture1y $.locale}}</div>
<div>Now: {{TimeSince $.Context .TimeNow $.locale}}</div>
<div>5s past: {{TimeSince $.Context .TimePast5s $.locale}}</div>
<div>5s future: {{TimeSince $.Context .TimeFuture5s $.locale}}</div>
<div>2m past: {{TimeSince $.Context .TimePast2m $.locale}}</div>
<div>2m future: {{TimeSince $.Context .TimeFuture2m $.locale}}</div>
<div>1y past: {{TimeSince $.Context .TimePast1y $.locale}}</div>
<div>1y future: {{TimeSince $.Context .TimeFuture1y $.locale}}</div>
</div>

<div>
Expand Down
2 changes: 1 addition & 1 deletion templates/explore/repo_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
{{end}}
</div>
{{end}}
<p class="time">{{$.locale.Tr "org.repo_updated"}} {{TimeSinceUnix .UpdatedUnix $.locale}}</p>
<p class="time">{{$.locale.Tr "org.repo_updated"}} {{TimeSinceUnix $.Context .UpdatedUnix $.locale}}</p>
</div>
</div>
{{else}}
Expand Down
2 changes: 1 addition & 1 deletion templates/package/shared/list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<span class="ui label">{{svg .Package.Type.SVGName 16}} {{.Package.Type.Name}}</span>
</div>
<div class="desc issue-item-bottom-row gt-df gt-ac gt-fw gt-my-1">
{{$timeStr := TimeSinceUnix .Version.CreatedUnix $.locale}}
{{$timeStr := TimeSinceUnix $.Context .Version.CreatedUnix $.locale}}
{{$hasRepositoryAccess := false}}
{{if .Repository}}
{{$hasRepositoryAccess = index $.RepositoryAccessMap .Repository.ID}}
Expand Down
2 changes: 1 addition & 1 deletion templates/package/shared/versionlist.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<a class="title" href="{{.FullWebLink}}">{{.Version.LowerVersion}}</a>
</div>
<div class="desc issue-item-bottom-row gt-df gt-ac gt-fw gt-my-1">
{{$.locale.Tr "packages.published_by" (TimeSinceUnix .Version.CreatedUnix $.locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
{{$.locale.Tr "packages.published_by" (TimeSinceUnix $.Context .Version.CreatedUnix $.locale) .Creator.HomeLink (.Creator.GetDisplayName | Escape) | Safe}}
</div>
</div>
</li>
Expand Down
4 changes: 2 additions & 2 deletions templates/package/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
<h1>{{.PackageDescriptor.Package.Name}} ({{.PackageDescriptor.Version.Version}})</h1>
</div>
<div>
{{$timeStr := TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.locale}}
{{$timeStr := TimeSinceUnix $.Context .PackageDescriptor.Version.CreatedUnix $.locale}}
{{if .HasRepositoryAccess}}
{{.locale.Tr "packages.published_by_in" $timeStr .PackageDescriptor.Creator.HomeLink (.PackageDescriptor.Creator.GetDisplayName | Escape) .PackageDescriptor.Repository.Link (.PackageDescriptor.Repository.FullName | Escape) | Safe}}
{{else}}
Expand Down Expand Up @@ -44,7 +44,7 @@
{{if .HasRepositoryAccess}}
<div class="item">{{svg "octicon-repo" 16 "gt-mr-3"}} <a href="{{.PackageDescriptor.Repository.Link}}">{{.PackageDescriptor.Repository.FullName}}</a></div>
{{end}}
<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix .PackageDescriptor.Version.CreatedUnix $.locale}}</div>
<div class="item">{{svg "octicon-calendar" 16 "gt-mr-3"}} {{TimeSinceUnix $.Context .PackageDescriptor.Version.CreatedUnix $.locale}}</div>
<div class="item">{{svg "octicon-download" 16 "gt-mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
{{template "package/metadata/cargo" .}}
{{template "package/metadata/chef" .}}
Expand Down
2 changes: 1 addition & 1 deletion templates/projects/list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<li class="item">
{{svg .IconName}} <a href="{{.Link}}">{{.Title}}</a>
<div class="meta">
{{$closedDate:= TimeSinceUnix .ClosedDateUnix $.locale}}
{{$closedDate:= TimeSinceUnix $.Context .ClosedDateUnix $.locale}}
{{if .IsClosed}}
{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.closed" $closedDate | Safe}}
{{end}}
Expand Down
2 changes: 1 addition & 1 deletion templates/projects/view.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@
<div class="meta gt-my-2">
<span class="text light grey">
{{.Repo.FullName}}#{{.Index}}
{{$timeStr := TimeSinceUnix .GetLastEventTimestamp $.locale}}
{{$timeStr := TimeSinceUnix $.Context .GetLastEventTimestamp $.locale}}
{{if .OriginalAuthor}}
{{$.locale.Tr .GetLastEventLabelFake $timeStr (.OriginalAuthor|Escape) | Safe}}
{{else if gt .Poster.ID 0}}
Expand Down
2 changes: 1 addition & 1 deletion templates/repo/actions/runs_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</div>
</div>
<div class="issue-item-right">
<div>{{TimeSinceUnix .Updated $.locale}}</div>
<div>{{TimeSinceUnix $.Context .Updated $.locale}}</div>
<div>{{.Duration}}</div>
</div>
</li>
Expand Down
Loading