Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable30] fix: e-mail guests - frontend 📧 (part 1) #13663

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading