Skip to content

Commit b14fc59

Browse files
authored
Merge pull request #5059 from nextcloud-libraries/feat/user-status-icon
feat: Add NcUserStatusIcon
2 parents 07862b4 + 57c3eb6 commit b14fc59

File tree

12 files changed

+255
-61
lines changed

12 files changed

+255
-61
lines changed

.eslintrc.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,12 @@ module.exports = {
2121
],
2222
},
2323
},
24+
rules: {
25+
'import/no-unresolved': [
26+
'error',
27+
{
28+
ignore: ['\\?raw$'],
29+
},
30+
],
31+
},
2432
}

l10n/messages.pot

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ msgstr ""
6060
msgid "Boston Blue"
6161
msgstr ""
6262

63+
msgid "busy"
64+
msgstr ""
65+
6366
msgid "Cancel changes"
6467
msgstr ""
6568

Lines changed: 4 additions & 1 deletion
Loading
Lines changed: 5 additions & 1 deletion
Loading
Lines changed: 4 additions & 1 deletion
Loading
Lines changed: 3 additions & 1 deletion
Loading

src/components/NcAvatar/NcAvatar.vue

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,10 @@ export default {
167167
<span v-if="showUserStatusIconOnAvatar" class="avatardiv__user-status avatardiv__user-status--icon">
168168
{{ userStatus.icon }}
169169
</span>
170-
<NcIconSvgWrapper v-else-if="canDisplayUserStatus"
170+
<NcUserStatusIcon v-else-if="canDisplayUserStatus"
171171
class="avatardiv__user-status"
172-
:svg="userStatusIcon"
173-
:name="userStatusIconName" />
172+
:status="userStatus.status"
173+
:aria-hidden="String(hasMenu)" />
174174

175175
<!-- Show the letter if no avatar nor icon class -->
176176
<span v-if="showInitials"
@@ -188,9 +188,9 @@ import NcActions from '../NcActions/index.js'
188188
import NcActionLink from '../NcActionLink/index.js'
189189
import NcButton from '../NcButton/index.js'
190190
import NcLoadingIcon from '../NcLoadingIcon/index.js'
191-
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
191+
import NcUserStatusIcon from '../NcUserStatusIcon/index.js'
192192
import usernameToColor from '../../functions/usernameToColor/index.js'
193-
import { getUserStatusIcon, getUserStatusIconName, getUserStatusText } from '../../utils/UserStatus.ts'
193+
import { getUserStatusText } from '../../utils/UserStatus.ts'
194194
import { userStatus } from '../../mixins/index.js'
195195
import { t } from '../../l10n.js'
196196
@@ -238,7 +238,7 @@ export default {
238238
NcActionLink,
239239
NcButton,
240240
NcLoadingIcon,
241-
NcIconSvgWrapper,
241+
NcUserStatusIcon,
242242
},
243243
mixins: [userStatus],
244244
props: {
@@ -385,26 +385,10 @@ export default {
385385
}
386386
return t('Avatar of {displayName}', { displayName: this.displayName ?? this.user })
387387
},
388-
389-
userStatusIcon() {
390-
return getUserStatusIcon(this.userStatus.status)
391-
},
392-
393-
/**
394-
* If the avatar has no menu no aria-label is assigned, but for accessibility we still need the status to be accessible
395-
* So this sets the required accessible label for the user status icon.
396-
*/
397-
userStatusIconName() {
398-
// only needed if non-interactive, otherwise the aria-label is set
399-
if (this.hasMenu) {
400-
return
401-
}
402-
return getUserStatusIconName(this.userStatus.status)
403-
},
404388
canDisplayUserStatus() {
405389
return this.showUserStatus
406390
&& this.hasStatus
407-
&& ['online', 'away', 'dnd'].includes(this.userStatus.status)
391+
&& ['online', 'away', 'busy', 'dnd'].includes(this.userStatus.status)
408392
},
409393
showUserStatusIconOnAvatar() {
410394
return this.showUserStatus

src/components/NcRichContenteditable/NcAutoCompleteResult.vue

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,9 @@
2929
class="autocomplete-result__status autocomplete-result__status--icon">
3030
{{ status && status.icon || '' }}
3131
</span>
32-
<NcIconSvgWrapper v-else-if="status.status && status.status !== 'offline'"
32+
<NcUserStatusIcon v-else-if="status.status && status.status !== 'offline'"
3333
class="autocomplete-result__status"
34-
:svg="userStatusIcon"
35-
:name="userStatusIconName" />
34+
:status="status.status" />
3635
</div>
3736

3837
<!-- Title and subline -->
@@ -50,15 +49,13 @@
5049
<script>
5150
import { generateUrl } from '@nextcloud/router'
5251
53-
import NcIconSvgWrapper from '../NcIconSvgWrapper/index.js'
54-
55-
import { getUserStatusIcon, getUserStatusIconName } from '../../utils/UserStatus.ts'
52+
import NcUserStatusIcon from '../NcUserStatusIcon/index.js'
5653
5754
export default {
5855
name: 'NcAutoCompleteResult',
5956
6057
components: {
61-
NcIconSvgWrapper,
58+
NcUserStatusIcon,
6259
},
6360
6461
props: {
@@ -101,12 +98,6 @@ export default {
10198
? this.getAvatarUrl(this.id, 44)
10299
: null
103100
},
104-
userStatusIcon() {
105-
return getUserStatusIcon(this.status.status)
106-
},
107-
userStatusIconName() {
108-
return getUserStatusIconName(this.status.status)
109-
},
110101
},
111102
112103
methods: {
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
<!--
2+
- @copyright 2024 Christopher Ng <chrng8@gmail.com>
3+
-
4+
- @author Christopher Ng <chrng8@gmail.com>
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+
<docs>
24+
### Description
25+
26+
This component displays a user status icon.
27+
28+
### Example
29+
30+
```vue
31+
<template>
32+
<div class="row">
33+
<NcUserStatusIcon status="online" />
34+
<NcUserStatusIcon status="away" />
35+
<NcUserStatusIcon status="dnd" />
36+
<NcUserStatusIcon status="invisible" />
37+
</div>
38+
</template>
39+
40+
<style>
41+
.row {
42+
display: flex;
43+
gap: 10px;
44+
}
45+
</style>
46+
```
47+
</docs>
48+
49+
<template>
50+
<span v-if="activeStatus"
51+
class="user-status-icon"
52+
:class="{
53+
'user-status-icon--invisible': ['invisible', 'offline'].includes(status),
54+
}"
55+
role="img"
56+
:aria-hidden="ariaHidden"
57+
:aria-label="ariaLabel"
58+
v-html="activeSvg" /> <!-- eslint-disable-line vue/no-v-html -->
59+
</template>
60+
61+
<script>
62+
import Vue from 'vue'
63+
import axios from '@nextcloud/axios'
64+
import { generateOcsUrl } from '@nextcloud/router'
65+
import { getCapabilities } from '@nextcloud/capabilities'
66+
67+
import onlineSvg from '../../assets/status-icons/user-status-online.svg?raw'
68+
import awaySvg from '../../assets/status-icons/user-status-away.svg?raw'
69+
import dndSvg from '../../assets/status-icons/user-status-dnd.svg?raw'
70+
import invisibleSvg from '../../assets/status-icons/user-status-invisible.svg?raw'
71+
72+
import { getUserStatusText } from '../../utils/UserStatus.ts'
73+
import { t } from '../../l10n.js'
74+
75+
export default {
76+
name: 'NcUserStatusIcon',
77+
78+
props: {
79+
/**
80+
* Set the user id to fetch the status
81+
*/
82+
user: {
83+
type: String,
84+
default: null,
85+
},
86+
87+
/**
88+
* Set the status
89+
*
90+
* @type {'online' | 'away' | 'busy' | 'dnd' | 'invisible' | 'offline'}
91+
*/
92+
status: {
93+
type: String,
94+
default: null,
95+
validator: (value) => [
96+
'online',
97+
'away',
98+
'busy',
99+
'dnd',
100+
'invisible',
101+
'offline',
102+
].includes(value),
103+
},
104+
105+
/**
106+
* Set the `aria-hidden` attribute
107+
*
108+
* @type {'true' | 'false'}
109+
*/
110+
ariaHidden: {
111+
type: String,
112+
default: null,
113+
validator: (value) => [
114+
'true',
115+
'false',
116+
].includes(value),
117+
},
118+
},
119+
120+
data() {
121+
return {
122+
fetchedUserStatus: null,
123+
}
124+
},
125+
126+
computed: {
127+
activeStatus() {
128+
return this.status ?? this.fetchedUserStatus
129+
},
130+
131+
activeSvg() {
132+
const matchSvg = {
133+
online: onlineSvg,
134+
away: awaySvg,
135+
busy: awaySvg,
136+
dnd: dndSvg,
137+
invisible: invisibleSvg,
138+
offline: invisibleSvg,
139+
}
140+
return matchSvg[this.activeStatus] ?? null
141+
},
142+
143+
ariaLabel() {
144+
if (this.ariaHidden === 'true') {
145+
return null
146+
}
147+
return t('User status: {status}', { status: getUserStatusText(this.activeStatus) })
148+
},
149+
},
150+
151+
watch: {
152+
user: {
153+
immediate: true,
154+
async handler(user, _oldUser) {
155+
if (!user || !getCapabilities()?.user_status?.enabled) {
156+
this.fetchedUserStatus = null
157+
return
158+
}
159+
try {
160+
const { data } = await axios.get(generateOcsUrl('/apps/user_status/api/v1/statuses/{user}', { user }))
161+
this.fetchedUserStatus = data.ocs?.data?.status
162+
} catch (error) {
163+
this.fetchedUserStatus = null
164+
}
165+
},
166+
},
167+
},
168+
169+
mounted() {
170+
if (!this.user && !this.status) {
171+
Vue.util.warn('[NcUserStatusIcon] The `user` or `status` prop should be set.')
172+
}
173+
},
174+
}
175+
</script>
176+
177+
<style lang="scss" scoped>
178+
.user-status-icon {
179+
display: flex;
180+
justify-content: center;
181+
align-items: center;
182+
min-width: 16px;
183+
min-height: 16px;
184+
max-width: 20px;
185+
max-height: 20px;
186+
187+
&--invisible {
188+
filter: var(--background-invert-if-dark);
189+
}
190+
}
191+
</style>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* @copyright 2024 Christopher Ng <chrng8@gmail.com>
3+
*
4+
* @author Christopher Ng <chrng8@gmail.com>
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+
export { default } from './NcUserStatusIcon.vue'

0 commit comments

Comments
 (0)