Skip to content

Commit

Permalink
fixed chat styling and scroll behaviour, implemented chat feed and us…
Browse files Browse the repository at this point in the history
…es socket for messages, logs and attachments, several chat functionality improvements
  • Loading branch information
roncodes committed Apr 12, 2024
1 parent cfb0bd6 commit 9ce981b
Show file tree
Hide file tree
Showing 14 changed files with 478 additions and 120 deletions.
4 changes: 4 additions & 0 deletions addon/components/chat-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,8 @@ import { inject as service } from '@ember/service';

export default class ChatContainerComponent extends Component {
@service chat;
constructor() {
super(...arguments);
this.chat.restoreOpenedChats();
}
}
1 change: 1 addition & 0 deletions addon/components/chat-tray.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default class ChatTrayComponent extends Component {
}

@action removeChannel(chatChannelRecord) {
this.chat.closeChannel(chatChannelRecord);
this.chat.deleteChatChannel(chatChannelRecord);
this.reloadChannels();
}
Expand Down
40 changes: 14 additions & 26 deletions addon/components/chat-window.hbs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="chat-window-container" {{did-insert this.positionWindow}} ...attributes>
<div class="chat-window-container {{if this.pendingAttachmentFiles "has-attachments"}}" {{did-insert this.positionWindow}} ...attributes>
<div class="chat-window-controls-container">
<div class="chat-window-name">
{{n-a this.channel.name "Untitled Chat"}}
Expand All @@ -11,7 +11,7 @@
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.addParticipant user)}}>
<div class="flex-1 flex flex-row items-center">
<div class="w-6">
<FaIcon @icon="user" />
<FaIcon @icon="user" @size="xs" />
</div>
<span>{{user.name}}</span>
</div>
Expand Down Expand Up @@ -39,31 +39,15 @@
</div>
{{/each}}
</div>
<div class="chat-window-messages-container" {{did-insert this.autoScrollMessagesWindow}}>
<div class="chat-window-messages-container" {{did-insert this.scrollMessageWindowBottom}}>
<div class="chat-window-messages">
{{#each this.channel.messages as |chatMessage|}}
<div class="chat-message-container">
<div class="chat-message-sender-bubble">
<Image src={{chatMessage.sender.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.sender.name}} />
<div class="chat-message-sender-name">{{chatMessage.sender.name}}</div>
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
<InputInfo @text={{chatMessage.sender.name}} />
</Attach::Tooltip>
</div>
<div class="chat-message-content-bubble-container">
<div class="chat-message-content-bubble {{if (eq chatMessage.sender_uuid this.currentUser.id) 'sender-bubble'}}">
{{chatMessage.content}}
</div>
<div class="chat-message-created-at">{{chatMessage.createdAgo}}</div>
</div>
</div>
{{/each}}
<ChatWindow::Feed @channel={{this.channel}} />
</div>
</div>
<div class="chat-window-input-container">
<div class="chat-window-input-container {{if this.pendingAttachmentFiles "has-attachments"}}">
<div class="chat-window-attachments-container">
<div class="chat-window-attachment-input">
<FileUpload @name={{this.customField.name}} @for={{this.customField.name}} @accept={{join "," this.acceptedFileTypes}} @multiple={{true}} @onFileAdded={{this.onFileAddedHandler}}>
<FileUpload @name={{this.customField.name}} @for={{this.customField.name}} @accept={{join "," this.acceptedFileTypes}} @multiple={{true}} @onFileAdded={{perform this.uploadAttachmentFile}}>
<a tabindex={{0}} class="btn btn-default btn-xs cursor-pointer">
<FaIcon @icon="paperclip" @size="sm" class="mr-2" />
<span>Add Attachment</span>
Expand All @@ -76,15 +60,19 @@
</div>
{{/if}}
</div>
{{#each this.pendingAttachmentFiles as |pendingFile|}}
<ChatWindow::PendingAttachment @file={{pendingFile}} @onRemove={{fn this.removePendingAttachmentFile pendingFile}} />
{{/each}}
{{#if this.pendingAttachmentFiles}}
<div class="chat-window-pending-attachments-container">
{{#each this.pendingAttachmentFiles as |pendingFile|}}
<ChatWindow::PendingAttachment @file={{pendingFile}} @onRemove={{fn this.removePendingAttachmentFile pendingFile}} />
{{/each}}
</div>
{{/if}}
</div>
<div class="chat-window-input-box">
<Textarea @value={{this.pendingMessageContent}} placeholder="Type your message here" class="chat-window-input" rows="3" />
</div>
<div class="chat-window-submit-container">
<Button @type="primary" @icon="paper-plane" @text="Send" @size="xs" @onClick={{this.sendMessage}} />
<Button @type="primary" @icon="paper-plane" @text="Send" @size="xs" @onClick={{perform this.sendMessage}} @disabled={{not this.pendingMessageContent}} @isLoading={{and (not this.sendMessage.isIdle) (not this.uploadAttachmentFile.isIdle)}} />
</div>
</div>
</div>
50 changes: 14 additions & 36 deletions addon/components/chat-window.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,19 @@ export default class ChatWindowComponent extends Component {
this.channel.reloadParticipants();
break;
case 'chat_message.created':
this.chat.insertMessageFromSocket(this.channel, socketEvent.data);
this.chat.insertChatMessageFromSocket(this.channel, socketEvent.data);
break;
case 'chat_log.created':
this.chat.insertChatLogFromSocket(this.channel, socketEvent.data);
break;
case 'chat_attachment.created':
this.chat.insertChatAttachmentFromSocket(this.channel, socketEvent.data);
break;
}
});
}

@action onFileAddedHandler(file) {
@task *uploadAttachmentFile(file) {
// since we have dropzone and upload button within dropzone validate the file state first
// as this method can be called twice from both functions
if (['queued', 'failed', 'timed_out', 'aborted'].indexOf(file.state) === -1) {
Expand All @@ -75,7 +81,7 @@ export default class ChatWindowComponent extends Component {
this.pendingAttachmentFile = file;

// Queue and upload immediatley
this.fetch.uploadFile.perform(
yield this.fetch.uploadFile.perform(
file,
{
path: `uploads/chat/${this.channel.id}/attachments`,
Expand All @@ -101,36 +107,11 @@ export default class ChatWindowComponent extends Component {
this.pendingAttachmentFiles.removeObject(pendingFile);
}

@action sendMessage() {
this.chat.sendMessage(this.channel, this.sender, this.pendingMessageContent).then((chatMessageRecord) => {
this.sendAttachments(chatMessageRecord);
});
@task *sendMessage() {
const attachments = this.pendingAttachmentFiles.map((file) => file.id);
yield this.chat.sendMessage(this.channel, this.sender, this.pendingMessageContent, attachments);
this.pendingMessageContent = '';
}

@action sendAttachments(chatMessageRecord) {
// create file attachments
const attachments = this.pendingAttachmentFiles.map((file) => {
const attachment = this.store.createRecord('chat-attachment', {
chat_channel_uuid: this.channel.id,
file_uuid: file.id,
sender_uuid: this.sender.id,
});

if (chatMessageRecord) {
attachment.set('chat_message_uuid', chatMessageRecord.id);
}

return attachment;
});

// clear pending attachments
this.pendingAttachmentFiles = [];

// save attachments
return all(attachments.map((_) => _.save())).then((response) => {
console.log(response);
});
}

@action closeChannel() {
Expand All @@ -156,7 +137,7 @@ export default class ChatWindowComponent extends Component {
@action positionWindow(chatWindowElement) {
const chatWindowWidth = chatWindowElement.offsetWidth;
const multiplier = this.chat.openChannels.length - 1;
const marginRight = (chatWindowWidth + 20) * multiplier;
const marginRight = multiplier === 0 ? 16 : (chatWindowWidth + 16) * multiplier;
chatWindowElement.style.marginRight = `${marginRight}px`;

// reposition when chat is closed
Expand All @@ -165,11 +146,8 @@ export default class ChatWindowComponent extends Component {
});
}

@action autoScrollMessagesWindow(messagesWindowContainerElement) {
@action scrollMessageWindowBottom(messagesWindowContainerElement) {
messagesWindowContainerElement.scrollTop = messagesWindowContainerElement.scrollHeight;
setInterval(() => {
messagesWindowContainerElement.scrollTop = messagesWindowContainerElement.scrollHeight;
}, 1000);
}

@task *loadAvailableUsers(params = {}) {
Expand Down
16 changes: 15 additions & 1 deletion addon/components/chat-window/attachment.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
{{yield}}
<a href="#" class="chat-attachment-container" {{on "click" this.download}}>
{{#if this.chatAttachment.isImage}}
<Image src={{this.chatAttachment.url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{this.chatAttachment.filename}} class="chat-attachment-image-preview" />
{{else}}
<div class="chat-attachment-file-preview">
<div class="file-icon file-icon-{{this.extension}}">
<FaIcon @icon={{this.icon}} />
</div>
<div class="chat-attachment-file-preview-filename">{{this.chatAttachment.filename}}</div>
</div>
{{/if}}
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
<InputInfo @text="Click to download attachment" />
</Attach::Tooltip>
</a>
46 changes: 45 additions & 1 deletion addon/components/chat-window/attachment.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,47 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import getWithDefault from '@fleetbase/ember-core/utils/get-with-default';

export default class ChatWindowAttachmentComponent extends Component {}
export default class ChatWindowAttachmentComponent extends Component {
@tracked chatAttachment;
@tracked icon;

constructor(owner, { record }) {
super(...arguments);
this.chatAttachment = record;
this.icon = this.getIcon(record);
}

@action download() {
return this.chatAttachment.download();
}

getExtension(chatAttachment) {
const filename = chatAttachment.filename;
const extensionMatch = filename.match(/\.(.+)$/);
return extensionMatch ? extensionMatch[1] : null;
}

getIcon(chatAttachment) {
this.extension = this.getExtension(chatAttachment);

return getWithDefault(
{
xlsx: 'file-excel',
xls: 'file-excel',
xlsb: 'file-excel',
xlsm: 'file-excel',
csv: 'file-spreadsheet',
tsv: 'file-spreadsheet',
docx: 'file-word',
docm: 'file-word',
pdf: 'file-pdf',
ppt: 'file-powerpoint',
pptx: 'file-powerpoint',
},
this.extension,
'file-alt'
);
}
}
8 changes: 7 additions & 1 deletion addon/components/chat-window/feed.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
{{yield}}
{{#if (has-block)}}
{{yield @channel.feed}}
{{else}}
{{#each @channel.feed as |item|}}
{{component (concat "chat-window/" item.type) record=item.record}}
{{/each}}
{{/if}}
9 changes: 8 additions & 1 deletion addon/components/chat-window/log.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
{{yield}}
<div class="chat-log-container">
<div class="chat-log-content-bubble-container">
<div class="chat-log-content-bubble">
{{this.chatLog.resolved_content}}
</div>
<div class="chat-log-created-at">{{this.chatLog.createdAgo}}</div>
</div>
</div>
9 changes: 8 additions & 1 deletion addon/components/chat-window/log.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class ChatWindowLogComponent extends Component {}
export default class ChatWindowLogComponent extends Component {
@tracked chatLog;
constructor(owner, { record }) {
super(...arguments);
this.chatLog = record;
}
}
23 changes: 22 additions & 1 deletion addon/components/chat-window/message.hbs
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
{{yield}}
<div class="chat-message-container {{if this.chatMessage.attachments "has-attachments"}}">
<div class="chat-message-sender-bubble">
<Image src={{this.chatMessage.sender.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{this.chatMessage.sender.name}} />
<div class="chat-message-sender-name">{{this.chatMessage.sender.name}}</div>
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
<InputInfo @text={{this.chatMessage.sender.name}} />
</Attach::Tooltip>
</div>
<div class="chat-message-content-bubble-container {{if this.chatMessage.attachments "has-attachments"}}">
{{#if this.chatMessage.attachments}}
<div class="chat-message-attachments-container">
{{#each this.chatMessage.attachments as |attachment|}}
<ChatWindow::Attachment @record={{attachment}} />
{{/each}}
</div>
{{/if}}
<div class="chat-message-content-bubble {{if (eq this.chatMessage.sender_uuid this.currentUser.id) 'sender-bubble'}}">
{{this.chatMessage.content}}
</div>
<div class="chat-message-created-at">{{this.chatMessage.createdAgo}}</div>
</div>
</div>
9 changes: 8 additions & 1 deletion addon/components/chat-window/message.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

export default class ChatWindowMessageComponent extends Component {}
export default class ChatWindowMessageComponent extends Component {
@tracked chatMessage;
constructor(owner, { record }) {
super(...arguments);
this.chatMessage = record;
}
}
26 changes: 6 additions & 20 deletions addon/components/chat-window/pending-attachment.hbs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div class="chat-window-pending-attachment">
<div class="chat-window-pending-attachment-preview">
{{#if this.isImage}}
<Image src={{@file.url}} alt={{@file.original_filename}} class="x-fleetbase-file-preview rounded-md shadow-sm" />
<Image src={{@file.url}} @fallbackSrc={{config "defaultValues.placeholderImage"}} alt={{@file.original_filename}} class="x-fleetbase-file-preview rounded-md shadow-sm" />
{{else}}
<div class="x-fleetbase-file-preview">
<FileIcon @file={{@file}} @hideExtension={{true}} @iconSize="2xl" />
Expand All @@ -12,24 +12,10 @@
{{truncate-filename @file.original_filename}}
</div>
<div class="chat-window-pending-attachment-actions">
<DropdownButton @dropdownId="x-fleetbase-file-actions-dropdown" @icon="ellipsis" @iconSize="xs" @iconPrefix={{@dropdownButtonIconPrefix}} @text={{@dropdownButtonText}} @size="xs" @horizontalPosition="left" @calculatePosition={{@dropdownButtonCalculatePosition}} @renderInPlace={{or @dropdownButtonRenderInPlace true}} @wrapperClass={{concat @dropdownButtonWrapperClass " " "next-nav-item-dropdown-button"}} @triggerClass={{@dropdownButtonTriggerClass}} @registerAPI={{@registerAPI}} @onInsert={{this.onDropdownButtonInsert}} as |dd|>
<div class="next-dd-menu mt-0i" role="menu" aria-orientation="vertical" aria-labelledby="user-menu">
<div class="px-1">
<div class="text-sm flex flex-row items-center px-3 py-1 rounded-md my-1 text-gray-800 dark:text-gray-300">
{{t "component.file.dropdown-label"}}
</div>
</div>
<div class="next-dd-menu-seperator"></div>
<div role="group" class="px-1">
{{!-- template-lint-disable no-nested-interactive --}}
<a href="javascript:;" role="menuitem" class="next-dd-item text-danger" {{on "click" (fn this.onDropdownItemClick "onRemove" dd)}}>
<span class="mr-1">
<FaIcon @icon="trash" @prefix={{@dropdownButtonIconPrefix}} />
</span>
{{t "common.delete"}}
</a>
</div>
</div>
</DropdownButton>
<a href="javascript:;" {{on "click" this.remove}}>
<span class="mr-1">
<FaIcon @icon="trash" class="text-red-500" />
</span>
</a>
</div>
</div>
10 changes: 3 additions & 7 deletions addon/components/chat-window/pending-attachment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,9 @@ export default class ChatWindowPendingAttachmentComponent extends Component {
this.isImage = this.isImageFile(file);
}

@action onDropdownItemClick(action, dd) {
if (typeof dd.actions === 'object' && typeof dd.actions.close === 'function') {
dd.actions.close();
}

if (typeof this.args[action] === 'function') {
this.args[action](this.file);
@action remove() {
if (typeof this.args.onRemove === 'function') {
this.args.onRemove(this.file);
}
}

Expand Down
Loading

0 comments on commit 9ce981b

Please sign in to comment.