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

Allow users to display local time in profile #27748

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Allow users to display local time in profile
  • Loading branch information
JakobDev committed Oct 23, 2023
commit 3634f82e22b6b5a06526f3af9b2f5ccb687ed769
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
/web_src/fomantic/build/** linguist-generated
/web_src/fomantic/_site/globals/site.variables linguist-language=Less
/web_src/js/vendor/** -text -eol linguist-vendored
/options/timezones.csv linguist-vendored
Dockerfile.* linguist-language=Dockerfile
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ help:
@echo " - fmt format the Go code"
@echo " - generate-license update license files"
@echo " - generate-gitignore update gitignore files"
@echo " - update-timezones updates the timezones"
@echo " - generate-manpage generate manpage"
@echo " - generate-swagger generate the swagger spec from code comments"
@echo " - swagger-validate check if the swagger spec is valid"
Expand Down Expand Up @@ -979,5 +980,9 @@ docker:
docker build --disable-content-trust=false -t $(DOCKER_REF) .
# support also build args docker build --build-arg GITEA_VERSION=v1.2.3 --build-arg TAGS="bindata sqlite sqlite_unlock_notify" .

.PHONY: update-timezones
update-timezones:
$(GO) run build/update-timezones.go

# This endif closes the if at the top of the file
endif
81 changes: 81 additions & 0 deletions build/update-timezones.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
//go:build ignore

package main

import (
"archive/zip"
"io"
"log"
"net/http"
"os"

"code.gitea.io/gitea/modules/util"
)

func main() {
const (
url = "https://timezonedb.com/files/TimeZoneDB.csv.zip"
prefix = "timezone-archive"
filename = "time_zone.csv"
)

file, err := os.CreateTemp(os.TempDir(), prefix)
if err != nil {
log.Fatalf("Failed to create temp file. %s", err)
}

defer util.Remove(file.Name())

req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}

resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("Failed to download archive. %s", err)
}

defer resp.Body.Close()

if _, err := io.Copy(file, resp.Body); err != nil {
log.Fatalf("Failed to copy archive to file. %s", err)
}

file.Close()

zf, err := zip.OpenReader(file.Name())
if err != nil {
log.Fatalf("Failed to open archive. %s", err)
}
defer zf.Close()

fi, err := zf.Open(filename)
if err != nil {
log.Fatalf("Failed to open file in archive. %s", err)
}
defer fi.Close()

fo, err := os.Create("options/timezones.csv")
if err != nil {
log.Fatalf("Failed to create file. %s", err)
}
defer fo.Close()

buf := make([]byte, 1024)
for {
// read a chunk
n, err := fi.Read(buf)
if err != nil && err != io.EOF {
log.Fatalf("Failed to read file. %s", err)
}
if n == 0 {
break
}

// write a chunk
if _, err := fo.Write(buf[:n]); err != nil {
log.Fatalf("Failed to write file. %s", err)
}
}
}
1 change: 1 addition & 0 deletions models/fixtures/user.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
repo_admin_change_team_access: false
theme: ""
keep_activity_private: false
time_zone_name: "Pacific/Honolulu"

-
id: 2
Expand Down
27 changes: 27 additions & 0 deletions models/user/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/timeutil"
timezone_module "code.gitea.io/gitea/modules/timezone"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/validation"

Expand Down Expand Up @@ -140,6 +141,11 @@ type User struct {
DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
Theme string `xorm:"NOT NULL DEFAULT ''"`
KeepActivityPrivate bool `xorm:"NOT NULL DEFAULT false"`

// Timezone
DisplayLocalTime bool `xorm:"NOT NULL DEFAULT false"`
TimeZoneName string `xorm:"NOT NULL DEFAULT ''"`
TimeZone *timezone_module.TimeZone `xorm:"-"`
}

func init() {
Expand Down Expand Up @@ -317,6 +323,27 @@ func (u *User) GenerateEmailActivateCode(email string) string {
return code
}

// LoadTimeZone loads the TimeZone
func (u *User) LoadTimeZone() error {
if u.TimeZone != nil {
return nil
}

if u.TimeZoneName == "" {
u.TimeZone = timezone_module.GetDefaultTimeZone()
return nil
}

zoneList, err := timezone_module.GetTimeZoneList()
if err != nil {
return err
}

u.TimeZone = zoneList.GetTimeZoneByNameDefault(u.TimeZoneName)

return nil
}

// GetUserFollowers returns range of user's followers.
func GetUserFollowers(ctx context.Context, u, viewer *User, listOptions db.ListOptions) ([]*User, int64, error) {
sess := db.GetEngine(ctx).
Expand Down
8 changes: 8 additions & 0 deletions modules/structs/miscellaneous.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ type LicenseTemplateInfo struct {
Body string `json:"body"`
}

// TimeZone is a time zone
type TimeZone struct {
Name string `json:"name"`
Offset int64 `json:"offset"`
OffsetString string `json:"offset_string"`
CurrentTime string `json:"current_time"`
}

// APIError is an api error with a message
type APIError struct {
Message string `json:"message"`
Expand Down
14 changes: 10 additions & 4 deletions modules/structs/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ type User struct {
Followers int `json:"followers_count"`
Following int `json:"following_count"`
StarredRepos int `json:"starred_repos_count"`

// timezone
DisplayLocalTime bool `json:"display_local_time"`
TimeZone *TimeZone `json:"timezone"`
}

// MarshalJSON implements the json.Marshaler interface for User, adding field(s) for backward compatibility
Expand All @@ -75,8 +79,9 @@ type UserSettings struct {
Theme string `json:"theme"`
DiffViewStyle string `json:"diff_view_style"`
// Privacy
HideEmail bool `json:"hide_email"`
HideActivity bool `json:"hide_activity"`
HideEmail bool `json:"hide_email"`
HideActivity bool `json:"hide_activity"`
TimeZoneName string `json:"timezone_name"`
}

// UserSettingsOptions represents options to change user settings
Expand All @@ -90,8 +95,9 @@ type UserSettingsOptions struct {
Theme *string `json:"theme"`
DiffViewStyle *string `json:"diff_view_style"`
// Privacy
HideEmail *bool `json:"hide_email"`
HideActivity *bool `json:"hide_activity"`
HideEmail *bool `json:"hide_email"`
HideActivity *bool `json:"hide_activity"`
TimeZoneName *string `json:"timezone_name"`
}

// RenameUserOption options when renaming a user
Expand Down
2 changes: 2 additions & 0 deletions modules/timeutil/datetime.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ func DateTime(format string, datetime any) template.HTML {
return template.HTML(fmt.Sprintf(`<relative-time format="datetime" year="numeric" month="long" day="numeric" weekday="" datetime="%s">%s</relative-time>`, 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))
case "shortTime":
return template.HTML(fmt.Sprintf(`<relative-time format="datetime" weekday="" year="" month="" day="" hour="numeric" minute="numeric" datetime="%s">%s</relative-time>`, datetimeEscaped, textEscaped))
}
panic(fmt.Sprintf("Unsupported format %s", format))
}
172 changes: 172 additions & 0 deletions modules/timezone/timezone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package timezone

import (
"bytes"
"encoding/csv"
"fmt"
"strconv"
"strings"
"time"

"code.gitea.io/gitea/modules/options"
)

var (
zoneListCache TimeZoneList
lastCacheUpdate int64
)

const (
unixDay = 60 * 60 * 24
)

type TimeZone struct {
Name string
Offset int64
}

// Returns the current time in this timezone
func (timeZone *TimeZone) CurrentTime() time.Time {
return time.Now().UTC().Add(time.Second * time.Duration(timeZone.Offset))
}

// returns the value of current time as RFC3339 string but without the timezone
func (timeZone *TimeZone) CurrentTimeString() string {
return strings.TrimSuffix(timeZone.CurrentTime().Format(time.RFC3339), "Z")
}

// Returns the offset as printable string e.g. +01:00
func (timeZone *TimeZone) OffsetString() string {
offsetCopy := timeZone.Offset

// We don't want it to be negative
if offsetCopy < 0 {
offsetCopy = offsetCopy * -1
}

offsetString := time.Unix(offsetCopy, 0).UTC().Format("15:04")

if timeZone.Offset > 0 {
return fmt.Sprintf("+%s", offsetString)
} else if timeZone.Offset < 0 {
return fmt.Sprintf("-%s", offsetString)
}

return offsetString
}

// Returns if the timezone has any data
func (timeZone *TimeZone) IsEmpty() bool {
return timeZone.Name == ""
}

type TimeZoneList []*TimeZone

// Returns the timezone with the given name. Retruns nil, if the timezone was not found
func (zoneList TimeZoneList) GetTimeZoneByName(name string) *TimeZone {
for _, zone := range zoneList {
if zone.Name == name {
return zone
}
}

return nil
}

// Same as GetTimeZoneByName but returns the value of GetDefaultTimeZone instead of nil if the timezone is not found
func (zoneList TimeZoneList) GetTimeZoneByNameDefault(name string) *TimeZone {
zone := zoneList.GetTimeZoneByName(name)

if zone == nil {
return GetDefaultTimeZone()
}

return zone
}

// Returns a list of all known timezones
func GetTimeZoneList() (TimeZoneList, error) {
// If we don't have a cache or the cache is older than 24 hours, we need to renew the cache
if zoneListCache == nil || (lastCacheUpdate+unixDay) <= time.Now().UTC().Unix() {
err := UpdateTimeZoneListCache()
if err != nil {
return nil, err
}
}

return zoneListCache, nil
}

// Update the timezone cache
func UpdateTimeZoneListCache() error {
content, err := options.AssetFS().ReadFile("timezones.csv")
if err != nil {
return fmt.Errorf("ReadFile: %v", err)
}

csvReader := csv.NewReader(bytes.NewBuffer(content))
data, err := csvReader.ReadAll()
if err != nil {
return err
}

const nameColumn = 0
const timeStartColumn = 3
const offsetColumn = 4

currentTime := time.Now().UTC().Unix()
currentZone := new(TimeZone)
lastLoopName := ""
currentName := ""

zoneList := make(TimeZoneList, 0)

for _, row := range data {
if currentName == row[nameColumn] {
continue
}

// If the last timezone was not added, we add it here
if lastLoopName != "" && lastLoopName != row[nameColumn] && !currentZone.IsEmpty() {
zoneList = append(zoneList, currentZone)
currentZone = new(TimeZone)
}

lastLoopName = row[nameColumn]

timeStart, err := strconv.ParseInt(row[timeStartColumn], 10, 64)
if err != nil {
return fmt.Errorf("Convert %s to int64: %v", row[timeStartColumn], err)
}

offset, err := strconv.ParseInt(row[offsetColumn], 10, 64)
if err != nil {
return fmt.Errorf("Convert %s to int64: %v", row[offsetColumn], err)
}

if currentTime < timeStart {
// If the start time of the current row is higher than the last, we use the timezone from the last run
currentName = row[nameColumn]
zoneList = append(zoneList, currentZone)
currentZone = new(TimeZone)
} else {
currentZone = &TimeZone{Name: row[nameColumn], Offset: offset}
}
}

lastCacheUpdate = time.Now().UTC().Unix()
zoneListCache = zoneList

return nil
}

// Returns the timezone for Europe/London
func GetDefaultTimeZone() *TimeZone {
return &TimeZone{
Name: "Europe/London",
Offset: 0,
}
}
Loading