Skip to content

Commit

Permalink
Merge pull request #13633 from nextcloud/feat/13606/email-guests-fron…
Browse files Browse the repository at this point in the history
…tend
  • Loading branch information
Antreesy authored Oct 29, 2024
2 parents 089172e + 9698c34 commit b751e6a
Show file tree
Hide file tree
Showing 10 changed files with 191 additions and 188 deletions.
122 changes: 46 additions & 76 deletions src/components/AvatarWrapper/AvatarWrapper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,23 @@
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { shallowMount } from '@vue/test-utils'
import { cloneDeep } from 'lodash'
import Vuex from 'vuex'

import { t } from '@nextcloud/l10n'

import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js'

import AvatarWrapper from './AvatarWrapper.vue'

import { AVATAR } from '../../constants.js'
import storeConfig from '../../store/storeConfig.js'
import { ATTENDEE, AVATAR } from '../../constants.js'

describe('AvatarWrapper.vue', () => {
let testStoreConfig
let store
const USER_ID = 'user-id'
const USER_NAME = 'John Doe'
const PRELOADED_USER_STATUS = { status: 'online', message: null, icon: null }

beforeEach(() => {
testStoreConfig = cloneDeep(storeConfig)
store = new Vuex.Store(testStoreConfig)
})

describe('render user avatar', () => {
test('component renders NcAvatar with standard size by default', () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: USER_NAME,
},
Expand All @@ -43,10 +32,22 @@ describe('AvatarWrapper.vue', () => {

test('component does not render NcAvatar for non-users', () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: 'emails',
source: 'emails',
name: 'Email Guest',
source: ATTENDEE.ACTOR_TYPE.EMAILS,
},
})

const avatar = wrapper.findComponent(NcAvatar)
expect(avatar.exists()).toBeFalsy()
})

test('component does not render NcAvatar for federated users', () => {
const wrapper = shallowMount(AvatarWrapper, {
propsData: {
token: 'XXXTOKENXXX',
name: 'Federated User',
source: ATTENDEE.ACTOR_TYPE.FEDERATED_USERS,
},
})

Expand All @@ -57,7 +58,6 @@ describe('AvatarWrapper.vue', () => {
test('component renders NcAvatar with specified size', () => {
const size = 22
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: USER_NAME,
size,
Expand All @@ -70,10 +70,10 @@ describe('AvatarWrapper.vue', () => {

test('component pass props to NcAvatar correctly', async () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
id: USER_ID,
name: USER_NAME,
source: ATTENDEE.ACTOR_TYPE.USERS,
showUserStatus: true,
preloadedUserStatus: PRELOADED_USER_STATUS,
},
Expand All @@ -92,75 +92,45 @@ describe('AvatarWrapper.vue', () => {
})

describe('render specific icons', () => {
test('component render emails icon properly', () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: 'emails',
source: 'emails',
},
})

const icon = wrapper.find('.icon')
expect(icon.exists()).toBeTruthy()
expect(icon.classes('icon-mail')).toBeTruthy()
})

test('component render groups icon properly', () => {
const testCases = [
[null, ATTENDEE.CHANGELOG_BOT_ID, 'Talk updates', ATTENDEE.ACTOR_TYPE.BOTS, 'icon-changelog'],
[null, 'federated_user/id', USER_NAME, ATTENDEE.ACTOR_TYPE.FEDERATED_USERS, 'icon-user'],
[null, 'guest/id', '', ATTENDEE.ACTOR_TYPE.GUESTS, 'icon-user'],
[null, 'guest/id', t('spreed', 'Guest'), ATTENDEE.ACTOR_TYPE.GUESTS, 'icon-user'],
[null, 'guest/id', t('spreed', 'Guest'), ATTENDEE.ACTOR_TYPE.EMAILS, 'icon-user'],
['new', 'guest/id', '[email protected]', ATTENDEE.ACTOR_TYPE.EMAILS, 'icon-mail'],
[null, 'sha-phone', '+12345...', ATTENDEE.ACTOR_TYPE.PHONES, 'icon-phone'],
[null, 'team/id', 'Team', ATTENDEE.ACTOR_TYPE.CIRCLES, 'icon-team'],
[null, 'group/id', 'Group', ATTENDEE.ACTOR_TYPE.GROUPS, 'icon-contacts'],
]

it.each(testCases)('renders for token \'%s\', id \'%s\', name \'%s\' and source \'%s\' icon \'%s\'', (token, id, name, source, result) => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: 'groups',
source: 'groups',
},
propsData: { token, id, name, source },
})

const icon = wrapper.find('.icon')
expect(icon.exists()).toBeTruthy()
expect(icon.classes('icon-contacts')).toBeTruthy()
const avatar = wrapper.find('.avatar')
expect(avatar.exists()).toBeTruthy()
expect(avatar.classes(result)).toBeTruthy()
})
})

describe('render guests', () => {
test('component render icon of guest properly', () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: t('spreed', 'Guest'),
source: 'guests',
},
})

const guest = wrapper.find('.guest')
expect(guest.exists()).toBeTruthy()
expect(guest.text()).toBe('?')
})
describe('render specific symbols', () => {
const testCases = [
['guest/id', USER_NAME, ATTENDEE.ACTOR_TYPE.GUESTS, USER_NAME.charAt(0)],
['guest/id', USER_NAME, ATTENDEE.ACTOR_TYPE.EMAILS, USER_NAME.charAt(0)],
['deleted_users', USER_NAME, ATTENDEE.ACTOR_TYPE.DELETED_USERS, 'X'],
['bot-id', USER_NAME, ATTENDEE.ACTOR_TYPE.BOTS, '>_'],
]

test('component render icon of guest with name properly', () => {
it.each(testCases)('renders for id \'%s\', name \'%s\' and source \'%s\' symbol \'%s\'', (id, name, source, result) => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: USER_NAME,
source: 'guests',
},
propsData: { name, source },
})

const guest = wrapper.find('.guest')
expect(guest.text()).toBe(USER_NAME.charAt(0))
})

test('component render icon of deleted user properly', () => {
const wrapper = shallowMount(AvatarWrapper, {
store,
propsData: {
name: USER_NAME,
source: 'deleted_users',
},
})

const deleted = wrapper.find('.guest')
expect(deleted.exists()).toBeTruthy()
expect(deleted.text()).toBe('X')
const avatar = wrapper.find('.avatar')
expect(avatar.exists()).toBeTruthy()
expect(avatar.text()).toBe(result)
})
})
})
54 changes: 28 additions & 26 deletions src/components/AvatarWrapper/AvatarWrapper.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<template>
<div class="avatar-wrapper" :class="avatarClass" :style="avatarStyle">
<div v-if="iconClass" class="avatar icon" :class="[iconClass]" />
<div v-else-if="isGuest || isDeletedUser" class="avatar guest">
<div v-else-if="isGuestOrDeletedUser" class="avatar guest">
{{ firstLetterOfGuestName }}
</div>
<div v-else-if="isBot" class="avatar bot">
Expand Down Expand Up @@ -144,26 +144,30 @@ export default {
computed: {
// Determines which icon is displayed
iconClass() {
if (!this.source || this.isUser || (this.isFederatedUser && this.token) || this.isBot || this.isGuest || this.isDeletedUser) {
if (!this.source) {
return ''
}
if (this.isFederatedUser) {
return 'icon-user'
}
if (this.source === ATTENDEE.ACTOR_TYPE.EMAILS) {
return 'icon-mail'
}
if (this.source === ATTENDEE.ACTOR_TYPE.PHONES) {
switch (this.source) {
case ATTENDEE.ACTOR_TYPE.USERS:
case ATTENDEE.ACTOR_TYPE.BRIDGED:
case ATTENDEE.ACTOR_TYPE.DELETED_USERS:
return ''
case ATTENDEE.ACTOR_TYPE.FEDERATED_USERS:
return this.token ? '' : 'icon-user'
case ATTENDEE.ACTOR_TYPE.EMAILS:
return this.token === 'new' ? 'icon-mail' : (this.hasCustomName ? '' : 'icon-user')
case ATTENDEE.ACTOR_TYPE.GUESTS:
return this.hasCustomName ? '' : 'icon-user'
case ATTENDEE.ACTOR_TYPE.PHONES:
return 'icon-phone'
}
if (this.source === ATTENDEE.ACTOR_TYPE.BOTS && this.id === ATTENDEE.CHANGELOG_BOT_ID) {
return 'icon-changelog'
}
if (this.source === ATTENDEE.ACTOR_TYPE.CIRCLES) {
case ATTENDEE.ACTOR_TYPE.BOTS:
return this.id === ATTENDEE.CHANGELOG_BOT_ID ? 'icon-changelog' : ''
case ATTENDEE.ACTOR_TYPE.CIRCLES:
return 'icon-team'
case ATTENDEE.ACTOR_TYPE.GROUPS:
default:
return 'icon-contacts'
}
// source: groups
return 'icon-contacts'
},
avatarClass() {
return {
Expand All @@ -179,27 +183,25 @@ export default {
'--condensed-overlap': this.condensedOverlap,
}
},
isUser() {
return this.source === ATTENDEE.ACTOR_TYPE.USERS || this.source === ATTENDEE.ACTOR_TYPE.BRIDGED
},
isFederatedUser() {
return this.source === ATTENDEE.ACTOR_TYPE.FEDERATED_USERS
},
isBot() {
return this.source === ATTENDEE.ACTOR_TYPE.BOTS && this.id !== ATTENDEE.CHANGELOG_BOT_ID
},
isGuest() {
return this.source === ATTENDEE.ACTOR_TYPE.GUESTS
isGuestOrDeletedUser() {
return [ATTENDEE.ACTOR_TYPE.GUESTS, ATTENDEE.ACTOR_TYPE.EMAILS, ATTENDEE.ACTOR_TYPE.DELETED_USERS]
.includes(this.source)
},
isDeletedUser() {
return this.source === 'deleted_users'
hasCustomName() {
return this.name?.trim() && this.name !== t('spreed', 'Guest')
},
firstLetterOfGuestName() {
if (this.isDeletedUser) {
if (this.source === ATTENDEE.ACTOR_TYPE.DELETED_USERS) {
return 'X'
} else {
return this.name.toUpperCase().charAt(0)
}
const customName = this.name?.trim() && this.name !== t('spreed', 'Guest') ? this.name : '?'
return customName.charAt(0)
},
avatarUrl() {
return getUserProxyAvatarOcsUrl(this.token, this.id, this.isDarkTheme, this.size > AVATAR.SIZE.MEDIUM ? 512 : 64)
Expand Down
7 changes: 1 addition & 6 deletions src/components/ConversationSettings/LinkShareSettings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,7 @@ export default {

async handleResendInvitations() {
this.isSendingInvitations = true
try {
await this.$store.dispatch('resendInvitations', { token: this.token })
showSuccess(t('spreed', 'Invitations sent'))
} catch (e) {
showError(t('spreed', 'Error occurred when sending invitations'))
}
await this.$store.dispatch('resendInvitations', { token: this.token })
this.isSendingInvitations = false
},
},
Expand Down
Loading

0 comments on commit b751e6a

Please sign in to comment.