diff --git a/addon/components/badge.hbs b/addon/components/badge.hbs index a6f9192..bc528c7 100644 --- a/addon/components/badge.hbs +++ b/addon/components/badge.hbs @@ -1,16 +1,18 @@
- - + + {{#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}} diff --git a/addon/components/chat-container.hbs b/addon/components/chat-container.hbs new file mode 100644 index 0000000..26819fe --- /dev/null +++ b/addon/components/chat-container.hbs @@ -0,0 +1,5 @@ +
+ {{#each this.chat.openChannels as |chatChannel|}} + + {{/each}} +
\ No newline at end of file diff --git a/addon/components/chat-container.js b/addon/components/chat-container.js new file mode 100644 index 0000000..f81ecf3 --- /dev/null +++ b/addon/components/chat-container.js @@ -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(); + } +} diff --git a/addon/components/chat-tray.hbs b/addon/components/chat-tray.hbs new file mode 100644 index 0000000..9ae4641 --- /dev/null +++ b/addon/components/chat-tray.hbs @@ -0,0 +1,55 @@ +
+ + +
+ + {{#if this.unreadCount}} +
{{this.unreadCount}}
+ {{/if}} +
+
+ +
+
+
+
+ {{#each this.channels as |channel|}} +
+ +
+ {{#if (eq channel.created_by_uuid this.currentUser.id)}} +
+ +
+ {{/if}} +
+
+ {{/each}} +
+
+
+
+
\ No newline at end of file diff --git a/addon/components/chat-tray.js b/addon/components/chat-tray.js new file mode 100644 index 0000000..8bafac4 --- /dev/null +++ b/addon/components/chat-tray.js @@ -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); + } + } + } +} diff --git a/addon/components/chat-window.hbs b/addon/components/chat-window.hbs new file mode 100644 index 0000000..fe5a071 --- /dev/null +++ b/addon/components/chat-window.hbs @@ -0,0 +1,84 @@ +{{#if this.isVisible}} +
+
+
+ {{n-a this.channel.name "Untitled Chat"}} + + + +
+
+ +
+
+ {{#each this.availableUsers as |user|}} + +
+ +
+ +
+ {{user.name}} +
+
+ {{/each}} +
+
+
+ +
+
+
+ {{#each this.channel.participants as |chatParticipant|}} +
+ {{chatParticipant.name}} + + + + {{#if (can-remove-chat-participant this.channel this.sender chatParticipant)}} + + {{/if}} +
+
+ {{/each}} +
+
+ +
+
+
+
+ + + + Add Attachment + + + {{#if this.pendingAttachmentFile}} +
+ + {{round this.pendingAttachmentFile.progress}}% +
+ {{/if}} +
+ {{#if this.pendingAttachmentFiles}} +
+ {{#each this.pendingAttachmentFiles as |pendingFile|}} + + {{/each}} +
+ {{/if}} +
+
+