Skip to content

Commit 6408c52

Browse files
committed
enh: Make the date time formatting reusable for applications
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
1 parent fd53abe commit 6408c52

File tree

5 files changed

+227
-111
lines changed

5 files changed

+227
-111
lines changed

src/components/NcDateTime/NcDateTime.vue

Lines changed: 7 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,8 @@ h4 {
113113
</template>
114114

115115
<script>
116-
import { getCanonicalLocale } from '@nextcloud/l10n'
117-
import { t } from '../../l10n.js'
118-
119-
const FEW_SECONDS_AGO = {
120-
long: t('a few seconds ago'),
121-
short: t('seconds ago'), // FOR TRANSLATORS: Shorter version of 'a few seconds ago'
122-
narrow: t('sec. ago'), // FOR TRANSLATORS: If possible in your language an even shorter version of 'a few seconds ago'
123-
}
116+
import { computed } from 'vue'
117+
import { useFormatDateTime } from '../../composables/useFormatDateTime.js'
124118
125119
export default {
126120
name: 'NcDateTime',
@@ -164,99 +158,13 @@ export default {
164158
},
165159
},
166160
167-
data() {
161+
setup(props) {
162+
const timestamp = computed(() => props.timestamp)
163+
const { formattedTime, formattedFullTime } = useFormatDateTime(timestamp, props)
168164
return {
169-
/** Current time in ms */
170-
currentTime: Date.now(),
171-
/** ID of the current time interval */
172-
intervalId: undefined,
173-
}
174-
},
175-
176-
computed: {
177-
/** ECMA Date object of the timestamp */
178-
dateObject() {
179-
return new Date(this.timestamp)
180-
},
181-
/** Time string formatted for main text */
182-
formattedTime() {
183-
if (this.relativeTime !== false) {
184-
const formatter = new Intl.RelativeTimeFormat(getCanonicalLocale(), { numeric: 'auto', style: this.relativeTime })
185-
186-
const diff = this.dateObject - new Date(this.currentTime)
187-
const seconds = diff / 1000
188-
if (Math.abs(seconds) <= 90) {
189-
if (this.ignoreSeconds) {
190-
return FEW_SECONDS_AGO[this.relativeTime]
191-
} else {
192-
return formatter.format(Math.round(seconds), 'second')
193-
}
194-
}
195-
const minutes = seconds / 60
196-
if (Math.abs(minutes) <= 90) {
197-
return formatter.format(Math.round(minutes), 'minute')
198-
}
199-
const hours = minutes / 60
200-
if (Math.abs(hours) <= 24) {
201-
return formatter.format(Math.round(hours), 'hour')
202-
}
203-
const days = hours / 24
204-
if (Math.abs(days) <= 6) {
205-
return formatter.format(Math.round(days), 'day')
206-
}
207-
const weeks = days / 7
208-
if (Math.abs(weeks) <= 4) {
209-
return formatter.format(Math.round(weeks), 'week')
210-
}
211-
const months = days / 30
212-
if (Math.abs(months) <= 12) {
213-
return formatter.format(Math.round(months), 'month')
214-
}
215-
return formatter.format(Math.round(days / 365), 'year')
216-
}
217-
return this.formattedFullTime
218-
},
219-
formattedFullTime() {
220-
const formatter = new Intl.DateTimeFormat(getCanonicalLocale(), this.format)
221-
return formatter.format(this.dateObject)
222-
},
223-
},
224-
225-
watch: {
226-
/**
227-
* Set or clear interval if relative time is dis/enabled
228-
*
229-
* @param {boolean} newValue The new value of the relativeTime property
230-
* @param {boolean} _oldValue The old value of the relativeTime property
231-
*/
232-
relativeTime(newValue, _oldValue) {
233-
window.clearInterval(this.intervalId)
234-
this.intervalId = undefined
235-
if (newValue) {
236-
this.intervalId = window.setInterval(this.setCurrentTime, 1000)
237-
}
238-
},
239-
},
240-
241-
mounted() {
242-
// Start the interval for setting the current time if relative time is enabled
243-
if (this.relativeTime !== false) {
244-
this.intervalId = window.setInterval(this.setCurrentTime, 1000)
165+
formattedTime,
166+
formattedFullTime,
245167
}
246168
},
247-
248-
destroyed() {
249-
// ensure interval is cleared
250-
window.clearInterval(this.intervalId)
251-
},
252-
253-
methods: {
254-
/**
255-
* Set `currentTime` to the current timestamp, required as Date.now() is not reactive.
256-
*/
257-
setCurrentTime() {
258-
this.currentTime = Date.now()
259-
},
260-
},
261169
}
262170
</script>

src/composables/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@
2222

2323
export * from './useIsFullscreen/index.js'
2424
export * from './useIsMobile/index.js'
25+
export * from './useFormatDateTime.js'
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**
2+
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
3+
*
4+
* @author Ferdinand Thiessen <opensource@fthiessen.de>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import { getCanonicalLocale } from '@nextcloud/l10n'
24+
import { computed, onUnmounted, ref, onMounted, watch, unref } from 'vue'
25+
import { t } from '../l10n.js'
26+
27+
const FEW_SECONDS_AGO = {
28+
long: t('a few seconds ago'),
29+
short: t('seconds ago'), // FOR TRANSLATORS: Shorter version of 'a few seconds ago'
30+
narrow: t('sec. ago'), // FOR TRANSLATORS: If possible in your language an even shorter version of 'a few seconds ago'
31+
}
32+
33+
/**
34+
* Composable for formatting time stamps using current users locale
35+
*
36+
* @param {Date | number | import('vue').Ref<Date> | import('vue').Ref<number>} timestamp Current timestamp
37+
* @param {object} opts Optional options
38+
* @param {Intl.DateTimeFormatOptions} opts.format The format used for displaying, or if relative time is used the format used for the title (optional)
39+
* @param {boolean} opts.ignoreSeconds Ignore seconds when displaying the relative time and just show `a few seconds ago`
40+
* @param {false | 'long' | 'short' | 'narrow'} opts.relativeTime Wether to display the timestamp as time from now (optional)
41+
*/
42+
export function useFormatDateTime(timestamp = Date.now(), opts = {}) {
43+
// Current time as Date.now is not reactive
44+
const currentTime = ref(Date.now())
45+
// The interval ID for the window
46+
let intervalId = null
47+
48+
const options = ref({
49+
timeStyle: 'medium',
50+
dateStyle: 'short',
51+
relativeTime: 'long',
52+
ignoreSeconds: false,
53+
...unref(opts),
54+
})
55+
const wrappedOptions = computed(() => ({ ...unref(opts), ...options.value }))
56+
57+
/** ECMA Date object of the timestamp */
58+
const date = computed(() => new Date(unref(timestamp)))
59+
60+
const formattedFullTime = computed(() => {
61+
const formatter = new Intl.DateTimeFormat(getCanonicalLocale(), wrappedOptions.value.format)
62+
return formatter.format(date.value)
63+
})
64+
65+
/** Time string formatted for main text */
66+
const formattedTime = computed(() => {
67+
if (wrappedOptions.value.relativeTime !== false) {
68+
const formatter = new Intl.RelativeTimeFormat(getCanonicalLocale(), { numeric: 'auto', style: wrappedOptions.value.relativeTime })
69+
70+
const diff = date.value - currentTime.value
71+
const seconds = diff / 1000
72+
if (Math.abs(seconds) <= 90) {
73+
if (wrappedOptions.value.ignoreSeconds) {
74+
return FEW_SECONDS_AGO[wrappedOptions.value.relativeTime]
75+
} else {
76+
return formatter.format(Math.round(seconds), 'second')
77+
}
78+
}
79+
const minutes = seconds / 60
80+
if (Math.abs(minutes) <= 90) {
81+
return formatter.format(Math.round(minutes), 'minute')
82+
}
83+
const hours = minutes / 60
84+
if (Math.abs(hours) <= 24) {
85+
return formatter.format(Math.round(hours), 'hour')
86+
}
87+
const days = hours / 24
88+
if (Math.abs(days) <= 6) {
89+
return formatter.format(Math.round(days), 'day')
90+
}
91+
const weeks = days / 7
92+
if (Math.abs(weeks) <= 4) {
93+
return formatter.format(Math.round(weeks), 'week')
94+
}
95+
const months = days / 30
96+
if (Math.abs(months) <= 12) {
97+
return formatter.format(Math.round(months), 'month')
98+
}
99+
return formatter.format(Math.round(days / 365), 'year')
100+
}
101+
return formattedFullTime
102+
})
103+
104+
// Set or clear interval if relative time is dis/enabled
105+
watch([wrappedOptions], (opts) => {
106+
window.clearInterval(intervalId)
107+
intervalId = undefined
108+
if (opts.relativeTime) {
109+
intervalId = window.setInterval(() => { currentTime.value = new Date() }, 1000)
110+
}
111+
})
112+
113+
// Start the interval for setting the current time if relative time is enabled
114+
onMounted(() => {
115+
if (wrappedOptions.value.relativeTime !== false) {
116+
intervalId = window.setInterval(() => { currentTime.value = new Date() }, 1000)
117+
}
118+
})
119+
120+
// ensure interval is cleared
121+
onUnmounted(() => {
122+
window.clearInterval(intervalId)
123+
})
124+
125+
return {
126+
formattedTime,
127+
formattedFullTime,
128+
options,
129+
}
130+
}

tests/unit/components/NcDateTime/NcDateTime.spec.js

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,6 @@ describe('NcDateTime.vue', () => {
104104
},
105105
})
106106

107-
expect(wrapper.vm.currentTime).toEqual(time)
108107
expect(wrapper.element.textContent).toContain('now')
109108
})
110109

@@ -118,26 +117,24 @@ describe('NcDateTime.vue', () => {
118117
},
119118
})
120119

121-
expect(wrapper.vm.currentTime).toEqual(currentTime)
122120
expect(wrapper.element.textContent).toContain('3 seconds')
123121
currentTime = Date.UTC(2023, 5, 23, 14, 30, 34)
124122
// wait for timer
125123
await new Promise((resolve) => setTimeout(resolve, 1100))
126124
expect(wrapper.element.textContent).toContain('4 seconds')
127125
})
128126

129-
it('shows seconds from now - also as short variant', () => {
127+
it('shows seconds from now - also as narrow variant', () => {
130128
const time = Date.UTC(2023, 5, 23, 14, 30, 30)
131129
const currentTime = Date.UTC(2023, 5, 23, 14, 30, 33)
132130
Date.now = jest.fn(() => new Date(currentTime).valueOf())
133131
const wrapper = mount(NcDateTime, {
134132
propsData: {
135133
timestamp: time,
136-
relativeTime: 'short',
134+
relativeTime: 'narrow',
137135
},
138136
})
139137

140-
expect(wrapper.vm.currentTime).toEqual(currentTime)
141138
expect(wrapper.element.textContent).toContain('3 sec.')
142139
})
143140

@@ -151,7 +148,6 @@ describe('NcDateTime.vue', () => {
151148
},
152149
})
153150

154-
expect(wrapper.vm.currentTime).toEqual(currentTime)
155151
expect(wrapper.element.textContent).toContain('3 minutes')
156152
})
157153

@@ -165,7 +161,6 @@ describe('NcDateTime.vue', () => {
165161
},
166162
})
167163

168-
expect(wrapper.vm.currentTime).toEqual(currentTime)
169164
expect(wrapper.element.textContent).toContain('3 hours')
170165
})
171166

@@ -179,7 +174,6 @@ describe('NcDateTime.vue', () => {
179174
},
180175
})
181176

182-
expect(wrapper.vm.currentTime).toEqual(currentTime)
183177
expect(wrapper.element.textContent).toContain('2 days')
184178
})
185179

@@ -193,7 +187,6 @@ describe('NcDateTime.vue', () => {
193187
},
194188
})
195189

196-
expect(wrapper.vm.currentTime).toEqual(currentTime)
197190
expect(wrapper.element.textContent).toContain('3 weeks')
198191
})
199192

@@ -207,7 +200,6 @@ describe('NcDateTime.vue', () => {
207200
},
208201
})
209202

210-
expect(wrapper.vm.currentTime).toEqual(currentTime)
211203
expect(wrapper.element.textContent).toContain('5 months')
212204
})
213205

@@ -228,8 +220,6 @@ describe('NcDateTime.vue', () => {
228220
},
229221
})
230222

231-
expect(wrapper.vm.currentTime).toEqual(currentTime)
232-
expect(wrapper2.vm.currentTime).toEqual(currentTime)
233223
expect(wrapper.element.textContent).toContain('last year')
234224
expect(wrapper2.element.textContent).toContain('2 years')
235225
})

0 commit comments

Comments
 (0)