Skip to content
Open
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
22 changes: 22 additions & 0 deletions addon/components/utils/smart-conversation/message.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<div
class={{this.computedClasses}}
role="button"
data-control-name="smart-conversation-message"
{{on "click" this.toggleCollapsed}}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we can add in the splattributes just-in-case

>
<div class="content">
{{#if (eq @type "smart_reply")}}
<Utils::SmartBlob @size="md" />
{{/if}}

<div class="fx-col fx-gap-px-18">
<span>{{@value}}</span>

{{#if (has-block "extra-content")}}
{{yield to="extra-content"}}
{{/if}}
</div>
</div>

<span class="font-color-gray-400 font-size-xs">{{this.formattedTimestamp}}</span>
</div>
43 changes: 43 additions & 0 deletions addon/components/utils/smart-conversation/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { action } from '@ember/object';
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

import moment from 'moment';

interface UtilsSmartConversationMessageComponentSignature {
type: 'smart_reply' | 'user_prompt';
collapsible?: boolean;
value: string;
timestamp: number;
}

export default class UtilsSmartConversationMessageComponent extends Component<UtilsSmartConversationMessageComponentSignature> {
@tracked collapsed: boolean = true;

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');
}

@action
toggleCollapsed(event: MouseEvent): void {
event.stopPropagation();

if (!this.collapsible) return;

this.collapsed = !this.collapsed;
}

private get collapsible(): boolean {
return this.args.collapsible ?? true;
}
}
1 change: 1 addition & 0 deletions app/components/utils/smart-conversation/message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@upfluence/ember-upf-utils/components/utils/smart-conversation/message';
48 changes: 48 additions & 0 deletions app/styles/components/smart-conversation.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@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;
max-width: 70%;

.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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New font?

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);
}
}
}
1 change: 1 addition & 0 deletions app/styles/upf-utils.less
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
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);

module('User prompt', function (hooks) {
hooks.beforeEach(function () {
this.type = 'user_prompt';
this.value = 'Gimme, gimme, gimme a creator after midnight';
this.timestamp = moment('2026-05-05').valueOf();
});

test('it renders properly', async function (assert) {
await render(
hbs`<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

assert.dom('.smart-conversation-message').exists();
assert.dom('.smart-conversation-message').hasClass('smart-conversation-message--user_prompt');
assert.dom('.smart-conversation-message--collapsed').exists();
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');
});

test('the extra-content named block is rendered when provided', async function (assert) {
await render(
hbs`
<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}>
<:extra-content>
<div class="extra-content">Extra content</div>
</:extra-content>
</Utils::SmartConversation::Message>
`
);

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';
this.timestamp = moment('2026-04-22').valueOf();
});

test('it renders properly', async function (assert) {
await render(
hbs`<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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('22/04/2026, 00:00');
});

test('the extra-content named block is rendered when provided', async function (assert) {
await render(
hbs`
<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}>
<:extra-content>
<div class="extra-content">Extra content</div>
</:extra-content>
</Utils::SmartConversation::Message>
`
);

assert.dom('.extra-content').exists();
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`<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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`<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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`<Utils::SmartConversation::Message @collapsible={{false}} @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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`<Utils::SmartConversation::Message @collapsible={{false}} @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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`<Utils::SmartConversation::Message @collapsible={{true}} @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

assert.dom('.smart-conversation-message--collapsed').exists();
});

test('it toggles collapsed state on click', async function (assert) {
await render(
hbs`<Utils::SmartConversation::Message @type={{this.type}} @value={{this.value}} @timestamp={{this.timestamp}}/>`
);

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();
});
});
});
});
Loading