Skip to content

Commit 2dabce5

Browse files
holmesworcesterpdurbinadrastaea
authored
Feat/540-emoji-codes (#2776)
* Basic working emojicode replacement * replace as you type works, without it being over eager on :s as you type 😄, e.g. * fixes bug where hearts weren't tiny and black instead of big and red * adds a large emoji list * Adds basic tab completion for emojicodes * adds a nice dropdown for autocompletion * adds some passing cypress tests * Adds a storybook for messages with emojis * Removes emoticons beginning with > to avoid conflict with markdown, but leaves a note for posterity so they don't get added back when someone asks for them * Comments out emoticons whose quotes are being stripped by our linter --------- Shout out to agiledev24 )(whose work I did not end up using because it was easier to start fresh, but who took a first pass on this and got something working! <3) Co-authored-by: Philip Durbin <philipdurbin@gmail.com> Co-authored-by: Taea <88346289+adrastaea@users.noreply.github.com>
1 parent 02556f0 commit 2dabce5

17 files changed

Lines changed: 2061 additions & 32 deletions

CHANGELOG.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,16 @@
66

77
* Adds sticky date markers to the chat view [#505](https://github.com/TryQuiet/quiet/issues/505)
88
* Adds meaningful text to date markers, like "Today", "Yesterday", "Friday", or "Nov 30, 1999" [#2745](https://github.com/TryQuiet/quiet/issues/2745)
9+
* You can now type emoticons (<3) and emojicodes (:heart:) with tab completion and a handy dropdown. [#540](https://github.com/TryQuiet/quiet/issues/540) (thanks @agiledev24 for your initial work on this!)
10+
11+
### Fixes
12+
* Fixes an issue where heart emojis were displaying all tiny, ASCII, and goth. Now our hearts are big and bright red, for vibes! [#510](https://github.com/TryQuiet/quiet/issues/510)
13+
* Fixes back button navigation issues in user profile/edit screens [#2570](https://github.com/TryQuiet/quiet/issues/2570)
914

1015
### Chores
1116

1217
* Improves speed, reliability, and documentation for Cypress tests
1318

14-
### Fixes
15-
16-
* Fixes back button navigation issues in user profile/edit screens ([#2570]https://github.com/TryQuiet/quiet/issues/2570)
17-
1819
## [4.0.3]
1920

2021
### New features

packages/desktop/src/renderer/components/Channel/Channel.cy.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('Scroll behavior test', () => {
8080
})
8181

8282
it('PageUp keydown should scroll message list up.', () => {
83-
cy.get(messageInput).focus().type('{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}')
83+
cy.get(messageInput).focus().type('{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}{pageup}')
8484

8585
cy.get(channelContent).then($el => {
8686
const container = $el[0]
@@ -90,8 +90,13 @@ describe('Scroll behavior test', () => {
9090
})
9191

9292
it('PageDown keydown should scroll message list down.', () => {
93+
// note that Cypress UI before/after views do not correctly display the scroll position; to see the effect for debugging purposes, insert wait statements to slow down the test
9394
cy.get(channelContent).scrollTo(0, 0)
94-
cy.get(messageInput).focus().type('{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}')
95+
cy.get(messageInput)
96+
.focus()
97+
.type(
98+
'{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}{pagedown}'
99+
)
95100
cy.get(channelContent).assertScrolledToBottom()
96101
})
97102

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React from 'react'
2+
import CssBaseline from '@mui/material/CssBaseline'
3+
import { composeStories, setGlobalConfig } from '@storybook/testing-react'
4+
import { it, beforeEach, cy, Cypress, describe } from 'local-cypress'
5+
6+
import * as stories from './Channel.stories'
7+
import { withTheme } from '../../storybook/decorators'
8+
import { mount } from 'cypress/react18'
9+
10+
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
11+
Cypress.on('uncaught:exception', err => {
12+
// returning false here prevents Cypress from failing the test
13+
if (resizeObserverLoopErrRe.test(err.message)) {
14+
return false
15+
}
16+
})
17+
18+
// @ts-expect-error
19+
setGlobalConfig(withTheme)
20+
21+
// Use SendingMessagesWithScroll story to avoid TypeScript errors in other stories
22+
const { SendingMessagesWithScroll } = composeStories(stories)
23+
24+
describe('Emoji conversion in code blocks test', () => {
25+
beforeEach(() => {
26+
mount(
27+
<React.Fragment>
28+
<CssBaseline>
29+
<SendingMessagesWithScroll />
30+
</CssBaseline>
31+
</React.Fragment>
32+
)
33+
cy.wait(0)
34+
})
35+
36+
it('should NOT convert text typed inside an unclosed code fence', () => {
37+
cy.get('[data-testid="messageInput"]').type('```Some code :) ')
38+
39+
// The code fence is still open (no closing triple backticks).
40+
// So ":) " should remain literal and not become an emoji.
41+
cy.get('[data-testid="messageInput"]').should('have.value', '```Some code :) ')
42+
})
43+
44+
it('should convert text immediately after closing the code fence', () => {
45+
cy.get('[data-testid="messageInput"]')
46+
// Start an open code fence
47+
.type('```Inside code block :smile:')
48+
// Still open => :smile: remains literal
49+
.should('have.value', '```Inside code block :smile:')
50+
// Close the code block
51+
.type('``` ')
52+
// Now that fence is closed, the space after “``` ” is outside code block
53+
// Type a known emoticon
54+
.type(':p')
55+
.should('have.value', '```Inside code block :smile:``` :p')
56+
// Type punctuation => triggers conversion of :p
57+
.type('.')
58+
59+
cy.get('[data-testid="messageInput"]').should('have.value', '```Inside code block :smile:``` 😛.')
60+
})
61+
62+
it('should convert text typed entirely outside code fences', () => {
63+
// Type something normal outside code block
64+
cy.get('[data-testid="messageInput"]').type('Hello :smile: ').should('have.value', 'Hello 😄 ')
65+
})
66+
67+
it('should handle multiple code fences correctly', () => {
68+
cy.get('[data-testid="messageInput"]')
69+
// First code fence
70+
.type('```Block1 :)``` code between ```Block2 :heart: ')
71+
72+
// "Block1 :)" is inside the first code fence => no conversion
73+
// "Block2 :heart:" is inside second code fence => no conversion yet
74+
cy.get('[data-testid="messageInput"]').should('have.value', '```Block1 :)``` code between ```Block2 :heart: ')
75+
76+
// close second code fence
77+
cy.get('[data-testid="messageInput"]').type('``` ')
78+
79+
// After closing the second fence, type a space + emoticon
80+
cy.get('[data-testid="messageInput"]').type(':p ')
81+
82+
// Now the :p should convert to 😛 because we’re outside all fences
83+
cy.get('[data-testid="messageInput"]').should(
84+
'have.value',
85+
'```Block1 :)``` code between ```Block2 :heart: ``` 😛 '
86+
)
87+
})
88+
})
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import React from 'react'
2+
import CssBaseline from '@mui/material/CssBaseline'
3+
import { composeStories, setGlobalConfig } from '@storybook/testing-react'
4+
import { it, beforeEach, cy, Cypress, describe } from 'local-cypress'
5+
6+
import * as stories from './Channel.stories'
7+
import { withTheme } from '../../storybook/decorators'
8+
import { mount } from 'cypress/react18'
9+
import { ArrowKeyStepper } from 'react-virtualized'
10+
11+
const resizeObserverLoopErrRe = /^[^(ResizeObserver loop limit exceeded)]/
12+
Cypress.on('uncaught:exception', err => {
13+
if (resizeObserverLoopErrRe.test(err.message)) {
14+
return false
15+
}
16+
})
17+
18+
// @ts-expect-error
19+
setGlobalConfig(withTheme)
20+
21+
// Use SendingMessagesWithScroll story to avoid TypeScript errors in other stories
22+
const { SendingMessagesWithScroll } = composeStories(stories)
23+
24+
describe('Emoji dropdown behavior', () => {
25+
// Tests for checking that the emoji dropdown appears and disappears as expected
26+
// - appears when typing colon + characters
27+
// - disappears with Tab key (emoji selection)
28+
// - disappears with mouse click (emoji selection)
29+
// - disappears with Enter key (message sending)
30+
// - disappears when clicking away
31+
// - disappears when typing non-emoji pattern
32+
beforeEach(() => {
33+
mount(
34+
<React.Fragment>
35+
<CssBaseline>
36+
<SendingMessagesWithScroll />
37+
</CssBaseline>
38+
</React.Fragment>
39+
)
40+
cy.wait(0)
41+
})
42+
43+
it('should show dropdown when typing colon followed by characters', () => {
44+
// Start typing an emoji code
45+
cy.get('[data-testid="messageInput"]').type(':sm')
46+
47+
// Verify the dropdown appears
48+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible').should('contain', ':smile:')
49+
})
50+
51+
it('should hide dropdown when selecting an emoji with Tab key', () => {
52+
// Start typing an emoji code
53+
cy.get('[data-testid="messageInput"]').type(':sm')
54+
55+
// Verify dropdown appears
56+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
57+
58+
// Instead of Tab, trigger the keydown event directly
59+
cy.get('[data-testid="messageInput"]').trigger('keydown', {
60+
key: 'Tab',
61+
keyCode: 9,
62+
which: 9,
63+
code: 'Tab',
64+
})
65+
66+
// Verify dropdown disappears
67+
cy.get('[data-testid="emoji-dropdown"]').should('not.exist')
68+
})
69+
70+
it('should hide dropdown when selecting an emoji with Enter key', () => {
71+
// Start typing an emoji code
72+
cy.get('[data-testid="messageInput"]').type(':sm')
73+
74+
// Verify dropdown appears
75+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
76+
77+
// Trigger the Enter keydown event directly
78+
cy.get('[data-testid="messageInput"]').trigger('keydown', {
79+
key: 'Enter',
80+
keyCode: 13,
81+
which: 13,
82+
code: 'Enter',
83+
})
84+
85+
// Verify dropdown disappears
86+
cy.get('[data-testid="emoji-dropdown"]').should('not.exist')
87+
})
88+
89+
it('should hide dropdown when clicking an emoji suggestion', () => {
90+
// Start typing an emoji code
91+
cy.get('[data-testid="messageInput"]').type(':sm')
92+
93+
// Verify dropdown appears
94+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
95+
96+
// Click the first emoji suggestion
97+
cy.get('[data-testid="emoji-dropdown"] > div').first().click()
98+
99+
// Verify dropdown disappears
100+
cy.get('[data-testid="emoji-dropdown"]').should('not.exist')
101+
102+
// Verify emoji was inserted (smiley emoji U+1F603)
103+
cy.get('[data-testid="messageInput"]').should('have.value', '😃')
104+
})
105+
106+
it('should hide dropdown when clicking away', () => {
107+
// Start typing an emoji code
108+
cy.get('[data-testid="messageInput"]').type(':sm')
109+
110+
// Verify dropdown appears
111+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
112+
113+
// Click away from the input
114+
cy.get('body').click()
115+
116+
// Verify dropdown no longer exists in the DOM
117+
cy.get('[data-testid="emoji-dropdown"]').should('not.exist')
118+
})
119+
120+
it('should scroll dropdown when navigating with arrow keys', () => {
121+
// Type ":h" to get a decent number of emoji suggestions without being too specific
122+
cy.get('[data-testid="messageInput"]').type(':h')
123+
124+
// Verify dropdown appears
125+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
126+
127+
// Check initial scroll position
128+
cy.get('[data-testid="emoji-dropdown"]').then($dropdown => {
129+
const initialScrollTop = $dropdown[0].scrollTop
130+
131+
// Add a data attribute to track initial scroll position
132+
$dropdown[0].setAttribute('data-initial-scroll', initialScrollTop.toString())
133+
134+
// Press arrow down key multiple times to navigate through suggestions
135+
for (let i = 0; i < 8; i++) {
136+
cy.get('[data-testid="messageInput"]').trigger('keydown', {
137+
key: 'ArrowDown',
138+
keyCode: 40,
139+
which: 40,
140+
code: 'ArrowDown',
141+
bubbles: true,
142+
})
143+
}
144+
145+
// After key presses, verify the dropdown is still visible
146+
cy.get('[data-testid="emoji-dropdown"]')
147+
.should('be.visible')
148+
.then($updatedDropdown => {
149+
// Get the initial scroll position from the data attribute
150+
const initialScroll = parseInt($updatedDropdown[0].getAttribute('data-initial-scroll') || '0')
151+
const currentScrollTop = $updatedDropdown[0].scrollTop
152+
153+
// If scrollTop changed, scrolling occurred
154+
expect(currentScrollTop).to.be.gte(initialScroll)
155+
})
156+
})
157+
})
158+
159+
it('should hide dropdown when typing text that no longer matches emoji pattern', () => {
160+
// Start typing an emoji code
161+
cy.get('[data-testid="messageInput"]').type(':sm')
162+
163+
// Verify dropdown appears
164+
cy.get('[data-testid="emoji-dropdown"]').should('be.visible')
165+
166+
// Type a space to break the emoji pattern
167+
cy.get('[data-testid="messageInput"]').type(' ')
168+
169+
// Verify dropdown disappears
170+
cy.get('[data-testid="emoji-dropdown"]').should('not.exist')
171+
})
172+
})

0 commit comments

Comments
 (0)