Skip to content

Commit

Permalink
ui: fix wallet birthday timezone
Browse files Browse the repository at this point in the history
When using a <input type='date'> element, it is tricky to get the time
zone correct when using the value, valueAsNumber, and valueAsDate
fields. This is because the browser will always display the date as UTC,
regardless of how you have set valueAsDate. Also, when getting the date
out with new Date(input.value), where the value is yyyy-mm-dd, the Date
parses as in UTC.

When the user is interacting with the input element, they are thinking
in local time. However, unless we offset the time that we set, it can
show an incorrect date. This change sets the value date string manually
to the local date. We could also offsets the Date that we assign to
valueAsDate so that it will show a local date, or similarly with
valueAsNumber, but this is harder to follow and we do not benefit from
finer precision that simply 12am of the current day.

When reading the selected date back into javascript, we also have to
interpret it as a local date string. We do this by forcing the Date
constructor to interpret the date as local by appending "T00:00" to the
date string from input.value. We also must use this trick when reading
the min and max date strings. We could instead use valueAsDate and
manually shift in the opposite direction as when we set it, but this
seems better.
  • Loading branch information
chappjc committed Mar 24, 2023
1 parent 208bea7 commit 660cfc7
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 10 deletions.
2 changes: 1 addition & 1 deletion client/asset/btc/btc.go
Expand Up @@ -550,7 +550,7 @@ type WalletConfig struct {
}

// AdjustedBirthday converts WalletConfig.Birthday to a time.Time, and adjusts
// it so that defaultWalletBirthday <= WalletConfig.Bithday <= now.
// it so that defaultWalletBirthday <= WalletConfig.Birthday <= now.
func (cfg *WalletConfig) AdjustedBirthday() time.Time {
bday := time.Unix(int64(cfg.Birthday), 0)
now := time.Now()
Expand Down
2 changes: 1 addition & 1 deletion client/webserver/site/src/html/bodybuilder.tmpl
Expand Up @@ -104,7 +104,7 @@
{{end}}

{{define "bottom"}}
<script src="/js/entry.js?v=YYNQOy1"></script>
<script src="/js/entry.js?v=EuBjPDD"></script>
</body>
</html>
{{end}}
38 changes: 30 additions & 8 deletions client/webserver/site/src/js/forms.ts
Expand Up @@ -544,7 +544,11 @@ export class WalletConfigForm {
}
input.max = getMinMaxVal(opt.max)
input.min = getMinMaxVal(opt.min)
input.valueAsDate = opt.default ? new Date(opt.default * 1000) : new Date()
const date = opt.default ? new Date(opt.default * 1000) : new Date()
// UI shows Dates in valueAsDate as UTC, but user interprets local. Set a
// local date string so the UI displays what the user expects. alt:
// input.valueAsDate = dateApplyOffset(date)
input.value = dateToString(date)
} else input.value = opt.default !== null ? opt.default : ''
input.disabled = Boolean(opt.disablewhenactive && this.assetHasActiveOrders)
return el
Expand Down Expand Up @@ -635,8 +639,10 @@ export class WalletConfigForm {
finds.push(el)
const input = Doc.safeSelector(el, 'input') as HTMLInputElement
if (opt.isboolean) input.checked = isTruthyString(v)
else if (opt.isdate) input.valueAsDate = new Date(parseInt(v) * 1000)
else input.value = v
else if (opt.isdate) {
input.value = dateToString(new Date(parseInt(v) * 1000))
// alt: input.valueAsDate = dateApplyOffset(...)
} else input.value = v
}
for (const r of removes) {
const i = this.configElements.indexOf(r)
Expand Down Expand Up @@ -671,9 +677,11 @@ export class WalletConfigForm {
if (opt.isboolean && opt.key) {
config[opt.key] = input.checked ? '1' : '0'
} else if (opt.isdate && opt.key) {
const minDate = input.min ? toUnixDate(new Date(input.min)) : Number.MIN_SAFE_INTEGER
const maxDate = input.max ? toUnixDate(new Date(input.max)) : Number.MAX_SAFE_INTEGER
let date = input.value ? toUnixDate(new Date(input.value)) : 0
// Force local time interpretation by appending a time to the date
// string, otherwise the Date constructor considers it UTC.
const minDate = input.min ? toUnixDate(new Date(input.min + 'T00:00')) : Number.MIN_SAFE_INTEGER
const maxDate = input.max ? toUnixDate(new Date(input.max + 'T00:00')) : Number.MAX_SAFE_INTEGER
let date = input.value ? toUnixDate(new Date(input.value + 'T00:00')) : 0
if (date < minDate) date = minDate
else if (date > maxDate) date = maxDate
config[opt.key] = '' + date
Expand Down Expand Up @@ -1856,7 +1864,21 @@ function toUnixDate (date: Date) {
return Math.floor(date.getTime() / 1000)
}

// dateToString converts a javascript date object to a YYYY-MM-DD format string.
// dateApplyOffset shifts a date by the timezone offset. This is used to make
// UTC dates show the local date. This can be used to prepare a Date so
// toISOString generates a local date string. This is also used to trick an html
// input element to show the local date when setting the valueAsDate field. When
// reading the date back to JS, the value field should be interpreted as local
// using the "T00:00" suffix, or the Date in valueAsDate should be shifted in
// the opposite direction.
function dateApplyOffset (date: Date) {
return new Date(date.getTime() - date.getTimezoneOffset() * 60 * 1000)
}

// dateToString converts a javascript date object to a YYYY-MM-DD format string,
// in the local time zone.
function dateToString (date: Date) {
return date.toISOString().split('T')[0]
return dateApplyOffset(date).toISOString().split('T')[0]
// Another common hack:
// date.toLocaleString("sv-SE", { year: "numeric", month: "2-digit", day: "2-digit" })
}

0 comments on commit 660cfc7

Please sign in to comment.