From b41b83b0ef778f0ff56f4c22e4488eed125ce09f Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 5 May 2026 13:12:10 +0200 Subject: [PATCH 1/6] add support for SmartConversation::Message component --- .../utils/smart-conversation/message.hbs | 22 +++++++++ .../utils/smart-conversation/message.ts | 40 ++++++++++++++++ .../utils/smart-conversation/message.js | 1 + app/styles/components/smart-conversation.less | 47 +++++++++++++++++++ app/styles/upf-utils.less | 1 + .../utils/smart-conversation/message-test.ts | 26 ++++++++++ 6 files changed, 137 insertions(+) create mode 100644 addon/components/utils/smart-conversation/message.hbs create mode 100644 addon/components/utils/smart-conversation/message.ts create mode 100644 app/components/utils/smart-conversation/message.js create mode 100644 app/styles/components/smart-conversation.less create mode 100644 tests/integration/components/utils/smart-conversation/message-test.ts diff --git a/addon/components/utils/smart-conversation/message.hbs b/addon/components/utils/smart-conversation/message.hbs new file mode 100644 index 00000000..976fc29b --- /dev/null +++ b/addon/components/utils/smart-conversation/message.hbs @@ -0,0 +1,22 @@ +
+
+ {{#if (eq @type "smart_reply")}} + + {{/if}} + +
+ {{@value}} + + {{#if (has-block "extra-content")}} + {{yield to="extra-content"}} + {{/if}} +
+
+ + {{this.formattedTimestamp}} +
diff --git a/addon/components/utils/smart-conversation/message.ts b/addon/components/utils/smart-conversation/message.ts new file mode 100644 index 00000000..8590c7d7 --- /dev/null +++ b/addon/components/utils/smart-conversation/message.ts @@ -0,0 +1,40 @@ +import Component from '@glimmer/component'; +import { tracked } from '@glimmer/tracking'; + +import moment from 'moment'; + +interface UtilsSmartConversationMessageComponentSignature { + type: 'smart_reply' | 'user_prompt'; + value: string; + timestamp: number; +} + +export default class UtilsSmartConversationMessageComponent extends Component { + @tracked collapsed: boolean = true; + + get collapsible(): boolean { + return this.args.type === 'smart_reply'; + } + + get computedClasses(): string { + const classes = ['smart-conversation-message', `smart-conversation-message--${this.args.type}`]; + + if (this.collapsible && this.collapsed) { + classes.push('smart-conversation-message--collapsed'); + } + + return classes.join(' '); + } + + get formattedTimestamp(): string { + return moment(this.args.timestamp).format('DD/MM/YYYY, HH:mm'); + } + + toggleCollapsed = (event: MouseEvent) => { + event.stopPropagation(); + + if (!this.collapsible) return; + + this.collapsed = !this.collapsed; + }; +} diff --git a/app/components/utils/smart-conversation/message.js b/app/components/utils/smart-conversation/message.js new file mode 100644 index 00000000..600c1baf --- /dev/null +++ b/app/components/utils/smart-conversation/message.js @@ -0,0 +1 @@ +export { default } from '@upfluence/ember-upf-utils/components/utils/smart-conversation/message'; diff --git a/app/styles/components/smart-conversation.less b/app/styles/components/smart-conversation.less new file mode 100644 index 00000000..c182aee9 --- /dev/null +++ b/app/styles/components/smart-conversation.less @@ -0,0 +1,47 @@ +@import url('https://fonts.googleapis.com/css?family=Reddit+Sans:wght@400,600&family=Open+Sans:400,400i,600,600i,700,700i&display=swap&subset=cyrillic,cyrillic-ext,greek,greek-ext,latin-ext,vietnamese'); + +.smart-conversation-message { + display: flex; + flex-direction: column; + gap: var(--spacing-px-3); + align-items: flex-end; + + .content { + border-radius: var(--border-radius-lg); + display: flex; + gap: var(--spacing-px-18); + padding: var(--spacing-px-12) var(--spacing-px-18); + font-size: var(--font-size-md); + font-family: 'Reddit Sans', sans-serif; + position: relative; + } + + &--smart_reply .content { + border: 1px solid transparent; + background: linear-gradient(45deg, var(--color-white) 0%, var(--color-primary-50) 100%) padding-box, + linear-gradient(90deg, var(--color-primary-100) 0%, var(--color-white) 72.5%) border-box; + + color: var(--color-primary-400); + } + + &--user_prompt .content { + background-color: var(--color-gray-100); + color: var(--color-gray-900); + padding: var(--spacing-px-12); + } + + &--collapsed .content { + max-height: 130px; + overflow-y: hidden; + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 40px; + background: linear-gradient(to top, var(--color-gray-50), transparent 40px); + } + } +} diff --git a/app/styles/upf-utils.less b/app/styles/upf-utils.less index 298ee52b..58e09df7 100644 --- a/app/styles/upf-utils.less +++ b/app/styles/upf-utils.less @@ -8,6 +8,7 @@ @import 'components/http-errors-code'; @import 'components/logo-maker'; @import 'components/smart-blob'; +@import 'components/smart-conversation'; @import 'components/uedit-file-uploader'; @import 'components/utm-link-builder'; @import 'components/utils/social-media-handler'; diff --git a/tests/integration/components/utils/smart-conversation/message-test.ts b/tests/integration/components/utils/smart-conversation/message-test.ts new file mode 100644 index 00000000..ab36721f --- /dev/null +++ b/tests/integration/components/utils/smart-conversation/message-test.ts @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | utils/smart-conversation/message', function (hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function (val) { ... }); + + await render(hbs``); + + assert.dom().hasText(''); + + // Template block usage: + await render(hbs` + + template block text + + `); + + assert.dom().hasText('template block text'); + }); +}); From c8ffcc6d383a3db3d28ef460bad27b7403879b0f Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 5 May 2026 13:27:23 +0200 Subject: [PATCH 2/6] add tests --- .../utils/smart-conversation/message-test.ts | 106 +++++++++++++++--- 1 file changed, 93 insertions(+), 13 deletions(-) diff --git a/tests/integration/components/utils/smart-conversation/message-test.ts b/tests/integration/components/utils/smart-conversation/message-test.ts index ab36721f..f11e68d0 100644 --- a/tests/integration/components/utils/smart-conversation/message-test.ts +++ b/tests/integration/components/utils/smart-conversation/message-test.ts @@ -1,26 +1,106 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; +import { click, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; +import moment from 'moment'; module('Integration | Component | utils/smart-conversation/message', function (hooks) { setupRenderingTest(hooks); - test('it renders', async function (assert) { - // Set any properties with this.set('myProperty', 'value'); - // Handle any actions with this.set('myAction', function (val) { ... }); + hooks.beforeEach(function () { + this.timestamp = moment('2026-05-05').valueOf(); + }); + + module('User prompt', function (hooks) { + hooks.beforeEach(function () { + this.type = 'user_prompt'; + this.value = 'This is a smart reply'; + }); + + test('it renders properly', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message').exists(); + assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--user_prompt'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + assert.dom('.smart-conversation-message .content').hasText(this.value); + assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('05/05/2026, 00:00'); + }); + + test('it is not collapsible at all', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + }); + + test('the extra-content named block is rendered when provided', async function (assert) { + await render( + hbs` + + <:extra-content> +
Extra content
+ +
+ ` + ); + + assert.dom('.extra-content').exists(); + assert.dom('.extra-content').hasText('Extra content'); + }); + }); + + module('Smart reply', function (hooks) { + hooks.beforeEach(function () { + this.type = 'smart_reply'; + this.value = 'This is a smart reply'; + }); + + test('it renders properly', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message').exists(); + assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--smart_reply'); + assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--collapsed'); + assert.dom('.smart-conversation-message .content').hasText(this.value); + assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('05/05/2026, 00:00'); + }); + + test('it toggles collapsed state on click', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').exists(); - await render(hbs``); + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); - assert.dom().hasText(''); + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').exists(); + }); - // Template block usage: - await render(hbs` - - template block text - - `); + test('the extra-content named block is rendered when provided', async function (assert) { + await render( + hbs` + + <:extra-content> +
Extra content
+ +
+ ` + ); - assert.dom().hasText('template block text'); + assert.dom('.extra-content').exists(); + assert.dom('.extra-content').hasText('Extra content'); + }); }); }); From 83989cf7832680473c47ced11ba111a14b3c384d Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 5 May 2026 14:19:11 +0200 Subject: [PATCH 3/6] add missing max-width --- app/styles/components/smart-conversation.less | 1 + 1 file changed, 1 insertion(+) diff --git a/app/styles/components/smart-conversation.less b/app/styles/components/smart-conversation.less index c182aee9..be4f75e9 100644 --- a/app/styles/components/smart-conversation.less +++ b/app/styles/components/smart-conversation.less @@ -5,6 +5,7 @@ flex-direction: column; gap: var(--spacing-px-3); align-items: flex-end; + max-width: 70%; .content { border-radius: var(--border-radius-lg); From b229c443e76c46fca7ae930cc0afecea94ae433e Mon Sep 17 00:00:00 2001 From: Philippe Ndiaye Date: Tue, 5 May 2026 14:44:47 +0200 Subject: [PATCH 4/6] fix pr feedbacks --- addon/components/utils/smart-conversation/message.ts | 6 ++++-- .../utils/smart-conversation/message-test.ts | 12 +++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/addon/components/utils/smart-conversation/message.ts b/addon/components/utils/smart-conversation/message.ts index 8590c7d7..4eb04929 100644 --- a/addon/components/utils/smart-conversation/message.ts +++ b/addon/components/utils/smart-conversation/message.ts @@ -1,3 +1,4 @@ +import { action } from '@ember/object'; import Component from '@glimmer/component'; import { tracked } from '@glimmer/tracking'; @@ -30,11 +31,12 @@ export default class UtilsSmartConversationMessageComponent extends Component { + @action + toggleCollapsed(event: MouseEvent): void { event.stopPropagation(); if (!this.collapsible) return; this.collapsed = !this.collapsed; - }; + } } diff --git a/tests/integration/components/utils/smart-conversation/message-test.ts b/tests/integration/components/utils/smart-conversation/message-test.ts index f11e68d0..293341b3 100644 --- a/tests/integration/components/utils/smart-conversation/message-test.ts +++ b/tests/integration/components/utils/smart-conversation/message-test.ts @@ -7,14 +7,11 @@ import moment from 'moment'; module('Integration | Component | utils/smart-conversation/message', function (hooks) { setupRenderingTest(hooks); - hooks.beforeEach(function () { - this.timestamp = moment('2026-05-05').valueOf(); - }); - module('User prompt', function (hooks) { hooks.beforeEach(function () { this.type = 'user_prompt'; - this.value = 'This is a smart reply'; + this.value = 'Gimme, gimme, gimme a creator after midnight'; + this.timestamp = moment('2026-05-05').valueOf(); }); test('it renders properly', async function (assert) { @@ -25,7 +22,7 @@ module('Integration | Component | utils/smart-conversation/message', function (h assert.dom('.smart-conversation-message').exists(); assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--user_prompt'); assert.dom('.smart-conversation-message--collapsed').doesNotExist(); - assert.dom('.smart-conversation-message .content').hasText(this.value); + assert.dom('.smart-conversation-message .content').hasText('Gimme, gimme, gimme a creator after midnight'); assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('05/05/2026, 00:00'); }); @@ -60,6 +57,7 @@ module('Integration | Component | utils/smart-conversation/message', function (h hooks.beforeEach(function () { this.type = 'smart_reply'; this.value = 'This is a smart reply'; + this.timestamp = moment('2026-04-22').valueOf(); }); test('it renders properly', async function (assert) { @@ -71,7 +69,7 @@ module('Integration | Component | utils/smart-conversation/message', function (h assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--smart_reply'); assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--collapsed'); assert.dom('.smart-conversation-message .content').hasText(this.value); - assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('05/05/2026, 00:00'); + assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('22/04/2026, 00:00'); }); test('it toggles collapsed state on click', async function (assert) { From 42951462205f48b807ed7a813c177b038a17a8d6 Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Tue, 12 May 2026 17:36:01 +0200 Subject: [PATCH 5/6] Convert collapsible getter into an optional arg (defaulted to true) --- .../utils/smart-conversation/message.ts | 3 +- .../utils/smart-conversation/message-test.ts | 95 ++++++++++++++----- 2 files changed, 71 insertions(+), 27 deletions(-) diff --git a/addon/components/utils/smart-conversation/message.ts b/addon/components/utils/smart-conversation/message.ts index 4eb04929..89bedf1c 100644 --- a/addon/components/utils/smart-conversation/message.ts +++ b/addon/components/utils/smart-conversation/message.ts @@ -6,6 +6,7 @@ import moment from 'moment'; interface UtilsSmartConversationMessageComponentSignature { type: 'smart_reply' | 'user_prompt'; + collapsible?: boolean; value: string; timestamp: number; } @@ -14,7 +15,7 @@ export default class UtilsSmartConversationMessageComponent extends Component` - ); - - assert.dom('.smart-conversation-message--collapsed').doesNotExist(); - - await click('.smart-conversation-message'); - assert.dom('.smart-conversation-message--collapsed').doesNotExist(); - }); - test('the extra-content named block is rendered when provided', async function (assert) { await render( hbs` @@ -72,20 +61,6 @@ module('Integration | Component | utils/smart-conversation/message', function (h assert.dom('.smart-conversation-message span.font-color-gray-400').hasText('22/04/2026, 00:00'); }); - test('it toggles collapsed state on click', async function (assert) { - await render( - hbs`` - ); - - assert.dom('.smart-conversation-message--collapsed').exists(); - - await click('.smart-conversation-message'); - assert.dom('.smart-conversation-message--collapsed').doesNotExist(); - - await click('.smart-conversation-message'); - assert.dom('.smart-conversation-message--collapsed').exists(); - }); - test('the extra-content named block is rendered when provided', async function (assert) { await render( hbs` @@ -101,4 +76,72 @@ module('Integration | Component | utils/smart-conversation/message', function (h assert.dom('.extra-content').hasText('Extra content'); }); }); + + module('Collapsible message', function () { + ['user_prompt', 'smart_reply'].forEach((type) => { + hooks.beforeEach(function () { + this.type = type; + }); + + test(`By default, message is collapsible for ${type}`, async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').exists(); + + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + }); + + test('when no @collapsible argument is provided, it defaults to true', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').exists(); + }); + + test('when a falsy @collapsible argument is provided, message is not collapsed', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + }); + + test('when a falsy @collapsible argument is provided, message is not collapsible', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + }); + + test('when a truthy @collapsible argument is provided, message is collapsed', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').exists(); + }); + + test('it toggles collapsed state on click', async function (assert) { + await render( + hbs`` + ); + + assert.dom('.smart-conversation-message--collapsed').exists(); + + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').doesNotExist(); + + await click('.smart-conversation-message'); + assert.dom('.smart-conversation-message--collapsed').exists(); + }); + }); + }); }); From bb91f47f1d8fb75c0dca4523fdb82bf1ce904370 Mon Sep 17 00:00:00 2001 From: Olympe Lespagnon Date: Tue, 12 May 2026 17:43:28 +0200 Subject: [PATCH 6/6] Make collapsible getter private --- addon/components/utils/smart-conversation/message.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/addon/components/utils/smart-conversation/message.ts b/addon/components/utils/smart-conversation/message.ts index 89bedf1c..74943e5d 100644 --- a/addon/components/utils/smart-conversation/message.ts +++ b/addon/components/utils/smart-conversation/message.ts @@ -14,10 +14,6 @@ interface UtilsSmartConversationMessageComponentSignature { export default class UtilsSmartConversationMessageComponent extends Component { @tracked collapsed: boolean = true; - get collapsible(): boolean { - return this.args.collapsible ?? true; - } - get computedClasses(): string { const classes = ['smart-conversation-message', `smart-conversation-message--${this.args.type}`]; @@ -40,4 +36,8 @@ export default class UtilsSmartConversationMessageComponent extends Component