Skip to content

Commit

Permalink
chat components are functional and ready for release v1
Browse files Browse the repository at this point in the history
  • Loading branch information
roncodes committed Apr 13, 2024
1 parent 52f5c1a commit ab78a93
Show file tree
Hide file tree
Showing 28 changed files with 688 additions and 148 deletions.
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
42 changes: 28 additions & 14 deletions addon/components/chat-tray.hbs
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
<div class="next-user-button" ...attributes>
<BasicDropdown @registerAPI={{this.registerAPI}} @defaultClass={{@wrapperClass}} @onOpen={{@onOpen}} @onClose={{@onClose}} @verticalPosition={{@verticalPosition}} @horizontalPosition={{@horizontalPosition}} @renderInPlace={{true}} @initiallyOpened={{@initiallyOpened}} as |dd|>
<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 flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<div class="next-org-button-trigger chat-tray-icon flex-shrink-0 {{if dd.isOpen 'is-open'}}">
<FaIcon @icon="message" />
{{!-- {{#if this.notifications.length}}
<div class="chat-tray-unread-notifications-badge">{{this.notifications.length}}</div>
{{/if}} --}}
{{#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" @onClick={{this.startChat}} />
<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" (fn this.openChannel channel)}}>
<div class="chat-tray-channel-preview-title font-bold mb-1">{{n-a channel.title "Untitled Chat"}}</div>
<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">{{channel.last_message.content}}</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">
<div class="btn-wrapper">
<button type="button" class="chat-tray-channel-preview-close-channel-btn btn btn-danger btn-xs cursor-default" {{on "click" (fn this.removeChannel channel)}}>
<FaIcon @icon="times" @size="sm" />
</button>
</div>
{{#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}}
Expand Down
170 changes: 163 additions & 7 deletions addon/components/chat-tray.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,104 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { inject as service } from '@ember/service';
import { tracked } from '@glimmer/tracking';
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();
this.reloadChannels({ relisten: true });
}

@action startChat() {
Expand All @@ -29,21 +108,98 @@ export default class ChatTrayComponent extends Component {
}

@action removeChannel(chatChannelRecord) {
this.chat.closeChannel(chatChannelRecord);
this.chat.deleteChatChannel(chatChannelRecord);
this.reloadChannels();
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();
}

reloadChannels() {
this.chat.loadChannels.perform({
@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);
}
}
}
}
Loading

0 comments on commit ab78a93

Please sign in to comment.