Skip to content

Commit

Permalink
fix(mentions): parse groups and federated user mentions, add test cov…
Browse files Browse the repository at this point in the history
…erage

Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
  • Loading branch information
Antreesy committed Feb 29, 2024
1 parent a91b44b commit bb12e7a
Show file tree
Hide file tree
Showing 8 changed files with 189 additions and 23 deletions.
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ module.exports = {
globalSetup: resolve(__dirname, 'jest.global.setup.js'),

collectCoverageFrom: [
'<rootDir>/src/**/*.{js,vue}',
'<rootDir>/src/**/*.{js,ts,vue}',
],

testEnvironment: 'jest-environment-jsdom',

moduleFileExtensions: [
'js',
'ts',
'vue',
],

Expand Down
2 changes: 1 addition & 1 deletion src/components/ConversationSettings/EditableTextField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import NcRichContenteditable from '@nextcloud/vue/dist/Components/NcRichContente
import NcRichText from '@nextcloud/vue/dist/Components/NcRichText.js'
import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js'

import { parseSpecialSymbols } from '../../utils/textParse.js'
import { parseSpecialSymbols } from '../../utils/textParse.ts'

export default {
name: 'EditableTextField',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ import { getMessageReminder, removeMessageReminder, setMessageReminder } from '.
import { copyConversationLinkToClipboard } from '../../../../../services/urlService.js'
import { useIntegrationsStore } from '../../../../../stores/integrations.js'
import { useReactionsStore } from '../../../../../stores/reactions.js'
import { parseMentions } from '../../../../../utils/textParse.js'
import { parseMentions } from '../../../../../utils/textParse.ts'

const EmojiIndex = new EmojiIndexFactory(data)
const supportReminders = getCapabilities()?.spreed?.features?.includes('remind-me-later')
Expand Down
2 changes: 1 addition & 1 deletion src/components/NewMessage/NewMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ import { useChatExtrasStore } from '../../stores/chatExtras.js'
import { useSettingsStore } from '../../stores/settings.js'
import { fetchClipboardContent } from '../../utils/clipboard.js'
import { isDarkTheme } from '../../utils/isDarkTheme.js'
import { parseSpecialSymbols } from '../../utils/textParse.js'
import { parseSpecialSymbols } from '../../utils/textParse.ts'

const disableKeyboardShortcuts = OCP.Accessibility.disableKeyboardShortcuts()
const supportTypingStatus = getCapabilities()?.spreed?.config?.chat?.['typing-privacy'] !== undefined
Expand Down
2 changes: 1 addition & 1 deletion src/stores/chatExtras.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import Vue from 'vue'

import { EventBus } from '../services/EventBus.js'
import { getUserAbsence } from '../services/participantsService.js'
import { parseSpecialSymbols, parseMentions } from '../utils/textParse.js'
import { parseSpecialSymbols, parseMentions } from '../utils/textParse.ts'

/**
* @typedef {string} Token
Expand Down
27 changes: 27 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ type ApiResponse<T> = Promise<{ data: T }>
// Conversations
export type Conversation = components['schemas']['Room']

// Chats
type ParamObject = {
id: string,
name: string,
type: string,
}
export type Mention = ParamObject & {
server?: string,
'call-type'?: string,
'icon-url'?: string,
}
type File = ParamObject & {
'size': number,
'path': string,
'link': string,
'etag': string,
'permissions': number,
'mimetype': string,
'preview-available': string,
'width': number,
'height': number,
}
type MessageParameters = Record<string, ParamObject | Mention | File>
export type ChatMessage = Omit<components['schemas']['ChatMessage'], 'messageParameters'> & {
messageParameters: MessageParameters
}

// Bots
export type Bot = components['schemas']['Bot']
export type BotWithDetails = components['schemas']['BotWithDetails']
Expand Down
130 changes: 130 additions & 0 deletions src/utils/__tests__/textParse.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { parseMentions, parseSpecialSymbols } from '../textParse.ts'

jest.mock('@nextcloud/router', () => ({
getBaseUrl: jest.fn().mockReturnValue('server2.com')
}))

describe('textParse', () => {
describe('parseMentions', () => {
it('replaces {mention-call} correctly', () => {
const input = 'test {mention-call1}'
const output = 'test @all'
const parameters = {
'mention-call1': {
id: 'room-id',
name: 'Room Display Name',
type: 'call',
},
}
expect(parseMentions(input, parameters)).toBe(output)
})

it('replaces multiple entries correctly', () => {
const input = 'test {mention-call1} test {mention-call1} test'
const output = 'test @all test @all test'
const parameters = {
'mention-call1': {
id: 'room-id',
name: 'Room Display Name',
type: 'call',
},
}
expect(parseMentions(input, parameters)).toBe(output)
})

it('replaces {mention-user} correctly', () => {
const input = 'test {mention-user1} test {mention-user2}'
const output = 'test @alice test @"alice space@mail.com"'
const parameters = {
'mention-user1': {
id: 'alice',
name: 'Just Alice',
type: 'user',
},
'mention-user2': {
id: 'alice space@mail.com',
name: 'Out of space Alice',
type: 'user',
}
}
expect(parseMentions(input, parameters)).toBe(output)
})

it('replaces {mention-group} correctly', () => {
const input = 'test {mention-group1} test {mention-group2}'
const output = 'test @"group/talk" test @"group/space talk"'
const parameters = {
'mention-group1': {
id: 'talk',
name: 'Talk Group',
type: 'user-group',
},
'mention-group2': {
id: 'space talk',
name: 'Out of space Talk Group',
type: 'user-group',
}
}
expect(parseMentions(input, parameters)).toBe(output)
})

it('replaces {mention-federated-user} correctly (for host and other federations)', () => {
const input = 'test {mention-federated-user1}'
const output = 'test @"federated_user/alice@server3.com"'
const parameters = {
'mention-federated-user1': {
id: 'alice',
name: 'Feder Alice',
type: 'user',
server: 'server3.com'
}
}
expect(parseMentions(input, parameters)).toBe(output)
})

it('replaces {mention-federated-user} correctly (for user from server2.com)', () => {
const input = 'test {mention-federated-user1}'
const output = 'test @"federated_user/alice@server2.com"'
const parameters = {
'mention-federated-user1': {
id: 'alice',
name: 'Feder Alice',
type: 'user',
}
}
expect(parseMentions(input, parameters)).toBe(output)
})
})

describe('parseSpecialSymbols', () => {
it('converts escaped HTML correctly', () => {
const input = '&lt;div&gt;Hello&amp;world&lt;/div&gt;'
const output = '<div>Hello&world</div>'
expect(parseSpecialSymbols(input)).toBe(output)
})

it('converts special characters correctly', () => {
const input = 'This is the &sect; symbol.'
const output = 'This is the § symbol.'
expect(parseSpecialSymbols(input)).toBe(output)
})

it('removes trailing and leading whitespaces', () => {
const input = ' Hello '
const output = 'Hello'
expect(parseSpecialSymbols(input)).toBe(output)
})

it('removes line breaks', () => {
const input = 'Hello\rworld\r\n!'
const output = 'Hello\nworld\n!'
expect(parseSpecialSymbols(input)).toBe(output)
})

it('returns the same text when there are no special symbols', () => {
const input = 'Hello world!'
const output = 'Hello world!'
expect(parseSpecialSymbols(input)).toBe(output)
})
})
})
44 changes: 26 additions & 18 deletions src/utils/textParse.js → src/utils/textParse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,34 @@
*
*/

import { getBaseUrl } from '@nextcloud/router'

import type { ChatMessage, Mention } from '../types'

/**
* Parse message text to return proper formatting for mentions
*
* @param {string} text The string to parse
* @param {object} parameters The parameters that contain the mentions
* @return {string}
* @param text The string to parse
* @param parameters The parameters that contain the mentions
*/
function parseMentions(text, parameters) {
if (Object.keys(parameters).some(key => key.startsWith('mention'))) {
for (const [key, value] of Object.entries(parameters)) {
let mention = ''
if (value?.type === 'call') {
mention = '@all'
} else if (value?.type === 'user') {
mention = value.id.includes(' ') ? `@"${value.id}"` : `@${value.id}`
}
if (mention) {
text = text.replace(new RegExp(`{${key}}`, 'g'), mention)
}
function parseMentions(text: string, parameters: ChatMessage['messageParameters']): string {
for (const key of Object.keys(parameters).filter(key => key.startsWith('mention'))) {
const value: Mention = parameters[key]
let mention = ''

if (key.startsWith('mention-call') && value.type === 'call') {
mention = '@all'
} else if (key.startsWith('mention-federated-user') && value.type === 'user') {
const server = value?.server ?? getBaseUrl().replace('https://', '')
mention = `@"federated_user/${value.id}@${server}"`
} else if (key.startsWith('mention-group') && value.type === 'user-group') {
mention = `@"group/${value.id}"`
} else if (key.startsWith('mention-user') && value.type === 'user') {
mention = value.id.includes(' ') ? `@"${value.id}"` : `@${value.id}`
}

if (mention) {
text = text.replace(new RegExp(`{${key}}`, 'g'), mention)
}
}
return text
Expand All @@ -49,10 +58,9 @@ function parseMentions(text, parameters) {
* Parse special symbols in text like &amp; &lt; &gt; &sect;
* FIXME upstream: https://github.com/nextcloud-libraries/nextcloud-vue/issues/4492
*
* @param {string} text The string to parse
* @return {string}
* @param text The string to parse
*/
function parseSpecialSymbols(text) {
function parseSpecialSymbols(text: string): string {
const temp = document.createElement('textarea')
temp.innerHTML = text.replace(/&/gmi, '&amp;')
text = temp.value.replace(/&amp;/gmi, '&').replace(/&lt;/gmi, '<')
Expand Down

0 comments on commit bb12e7a

Please sign in to comment.