Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v0.2.13 #48

Merged
merged 28 commits into from
Apr 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0b1bf60
created chat component
doljko Apr 2, 2024
f93c2c8
fixed did not appear button
doljko Apr 2, 2024
340ae0e
removed function
doljko Apr 3, 2024
5b8cb16
demo ui
doljko Apr 3, 2024
e28b758
Merge branch 'main' of github.com:fleetbase/ember-ui into feature/chat
roncodes Apr 3, 2024
ec1fa31
moved locale selector as tray componentm, created basic aht components
roncodes Apr 3, 2024
cd8e719
make sure chat windows are properly spaced
roncodes Apr 3, 2024
27ea3e7
small styling improvements, correct chat window positioning, fixed sp…
roncodes Apr 3, 2024
a27f5a1
wip: positioning windows after closing
roncodes Apr 3, 2024
6d1a594
added `senderIsCreator` property to know who is the creator of the ch…
roncodes Apr 4, 2024
556ebe4
showed all users in organization
doljko Apr 4, 2024
87635ae
removed channel
doljko Apr 5, 2024
b25cc33
added remove button
doljko Apr 5, 2024
297ba88
functional remove and add participant and socket listening for chat e…
roncodes Apr 5, 2024
5aa0c0d
chat window improvements
roncodes Apr 8, 2024
b02e485
ability to add attachments to messages, fixed chat-tray styling
roncodes Apr 8, 2024
cfb0bd6
minor styling updates
roncodes Apr 8, 2024
240c576
fix ui
doljko Apr 8, 2024
bc98742
fix attachment file ui
doljko Apr 8, 2024
84c1a90
now can render custom icon component from menuItem from universe regi…
roncodes Apr 11, 2024
9ce981b
fixed chat styling and scroll behaviour, implemented chat feed and us…
roncodes Apr 12, 2024
52f5c1a
Merge branch 'feature/chat' of github.com:fleetbase/ember-ui into fea…
roncodes Apr 12, 2024
ab78a93
chat components are functional and ready for release v1
roncodes Apr 13, 2024
bad0f39
removed the `<Scrollable />` component
roncodes Apr 13, 2024
42b4d08
fix noop() utility test
roncodes Apr 13, 2024
1cb57ab
v0.2.13
roncodes Apr 13, 2024
9de0535
Merge pull request #47 from fleetbase/feature/custom-icon-for-extensions
roncodes Apr 13, 2024
aa08737
Merge pull request #46 from fleetbase/feature/chat
roncodes Apr 13, 2024
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
16 changes: 9 additions & 7 deletions addon/components/badge.hbs
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
<div class="status-badge {{safe-dasherize (or @type @status)}}-status-badge" ...attributes>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium leading-4 whitespace-no-wrap {{@spanClass}}">
<svg class="mr-1.5 h-2 w-2 {{if @hideStatusDot "hidden"}}" fill="currentColor" viewBox="0 0 8 8">
<span class="inline-flex items-center {{unless @roundedFull "px-2 py-0.5 rounded" "badge-rounded-full"}} text-xs font-medium leading-4 whitespace-no-wrap {{@spanClass}}">
<svg class="{{unless @hideText "mr-1.5"}} h-2 w-2 {{if @hideStatusDot "hidden"}}" fill="currentColor" viewBox="0 0 8 8">
<circle cx="4" cy="4" r="3"></circle>
</svg>
{{#if (has-block)}}
{{yield @status}}
{{else}}
{{#if @disableHumanize}}
{{@status}}
{{else}}
{{safe-humanize @status}}
{{/if}}
{{#unless @hideText}}
{{#if @disableHumanize}}
{{@status}}
{{else}}
{{safe-humanize @status}}
{{/if}}
{{/unless}}
{{/if}}
{{#if @helpText}}
<Attach::Tooltip @class="clean" @animation="scale" @placement={{or @tooltipPlacement "top" }}>
Expand Down
5 changes: 5 additions & 0 deletions addon/components/chat-container.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<div class="chat-container" ...attributes>
{{#each this.chat.openChannels as |chatChannel|}}
<ChatWindow @channel={{chatChannel}} />
{{/each}}
</div>
10 changes: 10 additions & 0 deletions addon/components/chat-container.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';

export default class ChatContainerComponent extends Component {
@service chat;
constructor() {
super(...arguments);
this.chat.restoreOpenedChats();
}
}
55 changes: 55 additions & 0 deletions addon/components/chat-tray.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<div class="next-user-button" ...attributes>
<BasicDropdown @registerAPI={{this.registerAPI}} @defaultClass={{@wrapperClass}} @onOpen={{this.unlockAudio}} @onClose={{@onClose}} @verticalPosition={{@verticalPosition}} @horizontalPosition={{@horizontalPosition}} @renderInPlace={{true}} @initiallyOpened={{@initiallyOpened}} as |dd|>
<dd.Trigger class={{@triggerClass}}>
<div class="next-org-button-trigger chat-tray-icon flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<FaIcon @icon="message" />
{{#if this.unreadCount}}
<div class="chat-tray-unread-notifications-badge">{{this.unreadCount}}</div>
{{/if}}
</div>
</dd.Trigger>
<dd.Content class="chat-tray-panel-container">
<div class="chat-tray-panel">
<div class="p-4">
<Button @type="primary" @text="Start Chat" @icon="paper-plane" @onClick={{dropdown-fn dd this.startChat}} />
</div>
<div class="flex flex-col">
{{#each this.channels as |channel|}}
<div class="chat-tray-channel-preview flex items-start px-4 py-3 border-t dark:border-gray-700 border-gray-200">
<button type="button" class="chat-tray-channel-preview-btn flex flex-col flex-1 cursor-default" {{on "click" (dropdown-fn dd this.openChannel channel)}}>
<div class="flex items-center mb-2">
<div class="chat-tray-channel-preview-title flex self-start items-center font-bold">{{n-a channel.title "Untitled Chat"}}</div>
{{#if channel.unread_count}}
<Badge @status="info" @hideStatusDot={{true}} class="flex self-start ml-2">{{pluralize channel.unread_count "Unread"}}</Badge>
{{/if}}
</div>
<div class="flex flex-row">
<div class="w-10">
<Image src={{channel.last_message.sender.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{channel.last_message.sender.name}} class="chat-tray-channel-preview-avatar rounded-full shadow-sm w-8 h-8" />
</div>
<div class="chat-tray-channel-preview-last-message text-sm truncate dark:text-gray-200 text-gray-900">
<span>{{channel.last_message.content}}</span>
{{#if channel.last_message.attachments}}
<div class="chat-tray-channel-preview-last-message-attachments">
<FaIcon @icon="paperclip" @size="sm" class="mr-0.5" /> {{pluralize channel.last_message.attachments.length "Attachment"}}
</div>
{{/if}}
</div>
</div>
</button>
<div class="flex">
{{#if (eq channel.created_by_uuid this.currentUser.id)}}
<div class="btn-wrapper">
<button type="button" class="chat-tray-channel-preview-close-channel-btn btn btn-danger btn-xs cursor-default" {{on "click" (dropdown-fn dd this.removeChannel channel)}}>
<FaIcon @icon="times" @size="sm" />
</button>
</div>
{{/if}}
</div>
</div>
{{/each}}
</div>
</div>
</dd.Content>
</BasicDropdown>
</div>
205 changes: 205 additions & 0 deletions addon/components/chat-tray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { isNone } from '@ember/utils';
import { task } from 'ember-concurrency';
import noop from '../utils/noop';

export default class ChatTrayComponent extends Component {
@service chat;
@service socket;
@service fetch;
@service store;
@service modalsManager;
@service currentUser;
@tracked channels = [];
@tracked unreadCount = 0;
@tracked notificationSound = new Audio('/sounds/message-notification-sound.mp3');

constructor() {
super(...arguments);
this.chat.loadChannels.perform({
withChannels: (channels) => {
this.channels = channels;
this.countUnread(channels);
this.listenAllChatChannels(channels);
this.listenUserChannel();
},
});
}

willDestroy() {
this.chat.off('chat.feed_updated', this.reloadChannelWithDelay.bind(this));
super.willDestroy(...arguments);
}

listenAllChatChannels(channels) {
channels.forEach((chatChannelRecord) => {
this.listenChatChannel(chatChannelRecord);
});
}

async listenUserChannel() {
this.socket.listen(`user.${this.currentUser.id}`, (socketEvent) => {
const { event, data } = socketEvent;
switch (event) {
case 'chat.participant_added':
case 'chat_participant.created':
this.reloadChannels();
break;
case 'chat.participant_removed':
case 'chat_participant.deleted':
this.reloadChannels();
this.closeChannelIfRemovedFromParticipants(data);
break;
case 'chat_channel.created':
this.reloadChannels({ relisten: true });
this.openNewChannelAsParticipant(data);
break;
case 'chat_channel.deleted':
this.reloadChannels({ relisten: true });
this.closeChannelIfOpen(data);
break;
}
});
}

async listenChatChannel(chatChannelRecord) {
this.socket.listen(`chat.${chatChannelRecord.public_id}`, (socketEvent) => {
const { event, data } = socketEvent;
switch (event) {
case 'chat_message.created':
this.reloadChannels();
this.playSoundForIncomingMessage(chatChannelRecord, data);
break;
case 'chat.added_participant':
this.reloadChannels();
break;
case 'chat_participant.deleted':
case 'chat.removed_participant':
this.reloadChannels();
this.closeChannelIfRemovedFromParticipants(data);
break;
case 'chat_channel.created':
this.reloadChannels({ relisten: true });
this.openNewChannelAsParticipant(data);
break;
case 'chat_channel.deleted':
this.reloadChannels({ relisten: true });
this.closeChannelIfOpen(data);
break;
case 'chat_receipt.created':
this.reloadChannels({ relisten: true });
break;
}
});
}

@action openChannel(chatChannelRecord) {
this.chat.openChannel(chatChannelRecord);
this.reloadChannels({ relisten: true });
}

@action startChat() {
this.chat.createChatChannel('Untitled Chat').then((chatChannelRecord) => {
this.openChannel(chatChannelRecord);
});
}

@action removeChannel(chatChannelRecord) {
this.modalsManager.confirm({
title: `Are you sure you wish to end this chat (${chatChannelRecord.title})?`,
body: 'Once this chat is ended, it will no longer be accessible for anyone.',
confirm: (modal) => {
modal.startLoading();

this.chat.closeChannel(chatChannelRecord);
this.chat.deleteChatChannel(chatChannelRecord);
return this.reloadChannels();
},
});
}

@action updateChatChannel(chatChannelRecord) {
this.chat.deleteChatChannel(chatChannelRecord);
this.reloadChannels();
}

@action async unlockAudio() {
this.reloadChannels();
try {
this.notificationSound.play().catch(noop);
this.notificationSound.pause();
this.notificationSound.currentTime = 0;
} catch (error) {
noop();
}
}

@task *getUnreadCount() {
const { unreadCount } = yield this.fetch.get('chat-channels/unread-count');
if (!isNone(unreadCount)) {
this.unreadCount = unreadCount;
}
}

playSoundForIncomingMessage(chatChannelRecord, data) {
const sender = this.getSenderFromParticipants(chatChannelRecord);
const isNotSender = sender ? sender.id !== data.sender_uuid : false;
if (isNotSender) {
this.notificationSound.play();
}
}

getSenderFromParticipants(channel) {
const participants = channel.participants ?? [];
return participants.find((chatParticipant) => {
return chatParticipant.user_uuid === this.currentUser.id;
});
}

countUnread(channels) {
this.unreadCount = channels.reduce((total, channel) => total + channel.unread_count, 0);
}

reloadChannels(options = {}) {
return this.chat.loadChannels.perform({
withChannels: (channels) => {
this.channels = channels;
this.countUnread(channels);
if (options && options.relisten === true) {
this.listenAllChatChannels(channels);
}
},
});
}

openNewChannelAsParticipant(data) {
const normalized = this.store.normalize('chat-channel', data);
const channel = this.store.push(normalized);
if (channel && this.getSenderFromParticipants(channel)) {
this.notificationSound.play();
this.openChannel(channel);
}
}

closeChannelIfOpen(data) {
const normalized = this.store.normalize('chat-channel', data);
const channel = this.store.push(normalized);
if (channel) {
this.chat.closeChannel(channel);
}
}

closeChannelIfRemovedFromParticipants(data) {
const normalized = this.store.normalize('chat-participant', data);
const removedChatParticipant = this.store.push(normalized);
if (removedChatParticipant) {
const channel = this.store.peekRecord('chat-channel', removedChatParticipant.chat_channel_uuid);
if (channel) {
this.chat.closeChannel(channel);
}
}
}
}
84 changes: 84 additions & 0 deletions addon/components/chat-window.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{{#if this.isVisible}}
<div id={{concat "channel-" this.channel.id "-window"}} 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"}}
<a href="#" {{on "click" this.editChatName}} class="ml-2 hover:opacity-50">
<FaIcon @icon="pencil" @size="xs" />
</a>
</div>
<div id={{concat "channel-" this.channel.id "-controls"}} class="chat-window-controls">
<DropdownButton class="chat-window-button" @icon="user-plus" @size="sm" @iconPrefix="fas" @triggerClass="hidden md:flex" @disabled={{not this.availableUsers}} as |dd|>
<div class="next-dd-menu mt-1 mx-0" aria-labelledby="user-menu">
<div class="p-1">
{{#each this.availableUsers as |user|}}
<a href="javascript:;" class="next-dd-item" {{on "click" (dropdown-fn dd this.addParticipant user)}}>
<div class="flex-1 flex flex-row items-center">
<Badge @status={{if user.is_online "online" "offline"}} @hideText={{true}} @roundedFull={{true}} class="mr-2" />
<div class="w-6">
<FaIcon @icon="user" @size="xs" />
</div>
<span>{{user.name}}</span>
</div>
</a>
{{/each}}
</div>
</div>
</DropdownButton>
<button type="button" class="chat-window-button chat-window-close-button" {{on "click" this.closeChannel}}>
<FaIcon @icon="times" @size="sm" />
</button>
</div>
</div>
<div id={{concat "channel-" this.channel.id "-participants"}} class="chat-window-participants-container">
{{#each this.channel.participants as |chatParticipant|}}
<div class="chat-window-participant-bubble-container">
<Image src={{chatParticipant.avatar_url}} @fallbackSrc={{config "defaultValues.userImage"}} alt={{chatParticipant.name}} class="chat-window-participant-bubble" />
<Attach::Tooltip @class="clean" @animation="scale" @placement="top">
<InputInfo @text={{chatParticipant.name}} />
</Attach::Tooltip>
{{#if (can-remove-chat-participant this.channel this.sender chatParticipant)}}
<button type="button" class="chat-window-remove-participant" {{on "click" (fn this.removeParticipant chatParticipant)}}>
<FaIcon @icon="times" @size="sm" />
</button>
{{/if}}
<div class="chat-window-participant-online-status {{if chatParticipant.is_online "is-online"}}" />
</div>
{{/each}}
</div>
<div id={{concat "channel-" this.channel.id "-feed"}} class="chat-window-messages-container" {{did-insert this.scrollMessageWindowBottom}}>
<ChatWindow::Feed @channel={{this.channel}} @chatParticipant={{this.sender}} />
</div>
<div id={{concat "channel-" this.channel.id "-input"}} 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={{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>
</a>
</FileUpload>
{{#if this.pendingAttachmentFile}}
<div class="ml-2 flex items-center text-sm">
<Spinner class="dark:text-blue-400 text-blue-900" />
<span class="ml-2 text-xs dark:text-blue-400 text-blue-900">{{round this.pendingAttachmentFile.progress}}%</span>
</div>
{{/if}}
</div>
{{#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}} {{on "keypress" this.handleKeyPress}} 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={{perform this.sendMessage}} @disabled={{not this.pendingMessageContent}} @isLoading={{and (not this.sendMessage.isIdle) (not this.uploadAttachmentFile.isIdle)}} />
</div>
</div>
</div>
{{/if}}
Loading
Loading