Support displaying time in absolute format
This adds two config items: useAbsoluteTimeFormat (boolean) and absoluteTimeFormatMinAge (string, number + unit ('d'|'h'|'m'|'s')). When `useAbsoluteTimeFormat` is true, the Timeago component will display absolute time if the time is at least `absoluteTimeFormatMinAge` from now. If `longFormat` prop is true, the fully formatted time is displayed. Otherwise, the format is determined by the `time` prop: (1) if `time` is on the same day of now, display hour and minute; (2) if `time` is in the same month of now, display day and hour; (3) if `time` is in the same year of now, display month and day; (4) otherwise, display year and month. If it should display relative time, the format is the same as before.
This commit is contained in:
parent
1667f1330c
commit
b14ba17256
1
changelog.d/date-absolute.add
Normal file
1
changelog.d/date-absolute.add
Normal file
|
@ -0,0 +1 @@
|
|||
Support displaying time in absolute format
|
|
@ -217,6 +217,29 @@
|
|||
{{ $t('settings.no_rich_text_description') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
path="useAbsoluteTimeFormat"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.absolute_time_format') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<ul
|
||||
class="setting-list suboptions"
|
||||
v-if="mergedConfig.useAbsoluteTimeFormat"
|
||||
>
|
||||
<li>
|
||||
<UnitSetting
|
||||
path="absoluteTimeFormatMinAge"
|
||||
unit-set="time"
|
||||
:units="['s', 'm', 'h', 'd']"
|
||||
:min="0"
|
||||
>
|
||||
{{ $t('settings.absolute_time_format_min_age') }}
|
||||
</UnitSetting>
|
||||
</li>
|
||||
</ul>
|
||||
<h3>{{ $t('settings.attachments') }}</h3>
|
||||
<li>
|
||||
<BooleanSetting
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:datetime="time"
|
||||
:title="localeDateString"
|
||||
>
|
||||
{{ relativeTimeString }}
|
||||
{{ relativeOrAbsoluteTimeString }}
|
||||
</time>
|
||||
</template>
|
||||
|
||||
|
@ -16,16 +16,28 @@ export default {
|
|||
props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'],
|
||||
data () {
|
||||
return {
|
||||
relativeTimeMs: 0,
|
||||
relativeTime: { key: 'time.now', num: 0 },
|
||||
interval: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
localeDateString () {
|
||||
const browserLocale = localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
shouldUseAbsoluteTimeFormat () {
|
||||
if (!this.$store.getters.mergedConfig.useAbsoluteTimeFormat) {
|
||||
return false
|
||||
}
|
||||
return DateUtils.durationStrToMs(this.$store.getters.mergedConfig.absoluteTimeFormatMinAge) <= this.relativeTimeMs
|
||||
},
|
||||
browserLocale () {
|
||||
return localeService.internalToBrowserLocale(this.$i18n.locale)
|
||||
},
|
||||
timeAsDate () {
|
||||
return typeof this.time === 'string'
|
||||
? new Date(Date.parse(this.time)).toLocaleString(browserLocale)
|
||||
: this.time.toLocaleString(browserLocale)
|
||||
? new Date(Date.parse(this.time))
|
||||
: this.time
|
||||
},
|
||||
localeDateString () {
|
||||
return this.timeAsDate.toLocaleString(this.browserLocale)
|
||||
},
|
||||
relativeTimeString () {
|
||||
const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num])
|
||||
|
@ -35,6 +47,40 @@ export default {
|
|||
}
|
||||
|
||||
return timeString
|
||||
},
|
||||
absoluteTimeString () {
|
||||
if (this.longFormat) {
|
||||
return this.localeDateString
|
||||
}
|
||||
const now = new Date()
|
||||
const formatter = (() => {
|
||||
if (DateUtils.isSameDay(this.timeAsDate, now)) {
|
||||
return new Intl.DateTimeFormat(this.browserLocale, {
|
||||
minute: 'numeric',
|
||||
hour: 'numeric'
|
||||
})
|
||||
} else if (DateUtils.isSameMonth(this.timeAsDate, now)) {
|
||||
return new Intl.DateTimeFormat(this.browserLocale, {
|
||||
hour: 'numeric',
|
||||
day: 'numeric'
|
||||
})
|
||||
} else if (DateUtils.isSameYear(this.timeAsDate, now)) {
|
||||
return new Intl.DateTimeFormat(this.browserLocale, {
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
} else {
|
||||
return new Intl.DateTimeFormat(this.browserLocale, {
|
||||
year: 'numeric',
|
||||
month: 'short'
|
||||
})
|
||||
}
|
||||
})()
|
||||
|
||||
return formatter.format(this.timeAsDate)
|
||||
},
|
||||
relativeOrAbsoluteTimeString () {
|
||||
return this.shouldUseAbsoluteTimeFormat ? this.absoluteTimeString : this.relativeTimeString
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -54,6 +100,7 @@ export default {
|
|||
methods: {
|
||||
refreshRelativeTimeObject () {
|
||||
const nowThreshold = typeof this.nowThreshold === 'number' ? this.nowThreshold : 1
|
||||
this.relativeTimeMs = DateUtils.relativeTimeMs(this.time)
|
||||
this.relativeTime = this.longFormat
|
||||
? DateUtils.relativeTime(this.time, nowThreshold)
|
||||
: DateUtils.relativeTimeShort(this.time, nowThreshold)
|
||||
|
|
|
@ -506,6 +506,8 @@
|
|||
"autocomplete_select_first": "Automatically select the first candidate when autocomplete results are available",
|
||||
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
|
||||
"emoji_reactions_scale": "Reactions scale factor",
|
||||
"absolute_time_format": "Use absolute time format",
|
||||
"absolute_time_format_min_age": "Only use for time older than this amount of time",
|
||||
"export_theme": "Save preset",
|
||||
"filtering": "Filtering",
|
||||
"wordfilter": "Wordfilter",
|
||||
|
|
|
@ -180,7 +180,9 @@ export const defaultState = {
|
|||
autocompleteSelect: undefined, // instance default
|
||||
closingDrawerMarksAsSeen: undefined, // instance default
|
||||
unseenAtTop: undefined, // instance default
|
||||
ignoreInactionableSeen: undefined // instance default
|
||||
ignoreInactionableSeen: undefined, // instance default
|
||||
useAbsoluteTimeFormat: undefined, // instance defualt
|
||||
absoluteTimeFormatMinAge: undefined // instance default
|
||||
}
|
||||
|
||||
// caching the instance default properties
|
||||
|
|
|
@ -119,6 +119,8 @@ const defaultState = {
|
|||
closingDrawerMarksAsSeen: true,
|
||||
unseenAtTop: false,
|
||||
ignoreInactionableSeen: false,
|
||||
useAbsoluteTimeFormat: false,
|
||||
absoluteTimeFormatMinAge: '0d',
|
||||
|
||||
// Nasty stuff
|
||||
customEmoji: [],
|
||||
|
|
|
@ -6,10 +6,13 @@ export const WEEK = 7 * DAY
|
|||
export const MONTH = 30 * DAY
|
||||
export const YEAR = 365.25 * DAY
|
||||
|
||||
export const relativeTime = (date, nowThreshold = 1) => {
|
||||
export const relativeTimeMs = (date) => {
|
||||
if (typeof date === 'string') date = Date.parse(date)
|
||||
return Math.abs(Date.now() - date)
|
||||
}
|
||||
export const relativeTime = (date, nowThreshold = 1) => {
|
||||
const round = Date.now() > date ? Math.floor : Math.ceil
|
||||
const d = Math.abs(Date.now() - date)
|
||||
const d = relativeTimeMs(date)
|
||||
const r = { num: round(d / YEAR), key: 'time.unit.years' }
|
||||
if (d < nowThreshold * SECOND) {
|
||||
r.num = 0
|
||||
|
@ -57,3 +60,39 @@ export const secondsToUnit = (unit, amount) => {
|
|||
case 'days': return (1000 * amount) / DAY
|
||||
}
|
||||
}
|
||||
|
||||
export const isSameYear = (a, b) => {
|
||||
return a.getFullYear() === b.getFullYear()
|
||||
}
|
||||
|
||||
export const isSameMonth = (a, b) => {
|
||||
return a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth()
|
||||
}
|
||||
|
||||
export const isSameDay = (a, b) => {
|
||||
return a.getFullYear() === b.getFullYear() &&
|
||||
a.getMonth() === b.getMonth() &&
|
||||
a.getDate() === b.getDate()
|
||||
}
|
||||
|
||||
export const durationStrToMs = (str) => {
|
||||
if (typeof str !== 'string') {
|
||||
return 0
|
||||
}
|
||||
|
||||
const unit = str.replace(/[0-9,.]+/, '')
|
||||
const value = str.replace(/[^0-9,.]+/, '')
|
||||
switch (unit) {
|
||||
case 'd':
|
||||
return value * DAY
|
||||
case 'h':
|
||||
return value * HOUR
|
||||
case 'm':
|
||||
return value * MINUTE
|
||||
case 's':
|
||||
return value * SECOND
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue