Skip to content

Commit

Permalink
Merge pull request #38 from fleetbase/feature/chat
Browse files Browse the repository at this point in the history
Feature: Chat
  • Loading branch information
roncodes authored Apr 13, 2024
2 parents cd82493 + 0c8d482 commit f3d44cd
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 21 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ module.exports = {
'ember/no-incorrect-calls-with-inline-anonymous-functions': 'off',
'ember/no-private-routing-service': 'off',
'no-useless-escape': 'off',
'no-inner-declarations': 'off',
'n/no-unpublished-require': [
'error',
{
Expand Down
8 changes: 6 additions & 2 deletions addon/services/app-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ export default class AppCacheService extends Service {
return this;
}

@action get(key) {
return this.localCache.get(`${this.cachePrefix}${dasherize(key)}`);
@action get(key, defaultValue = null) {
const value = this.localCache.get(`${this.cachePrefix}${dasherize(key)}`);
if (value === undefined) {
return defaultValue;
}
return value;
}

@action has(key) {
Expand Down
263 changes: 263 additions & 0 deletions addon/services/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
import Service, { inject as service } from '@ember/service';
import Evented from '@ember/object/evented';
import { tracked } from '@glimmer/tracking';
import { isArray } from '@ember/array';
import { task } from 'ember-concurrency';
import { all } from 'rsvp';

export default class ChatService extends Service.extend(Evented) {
@service store;
@service currentUser;
@service appCache;
@tracked channels = [];
@tracked openChannels = [];

openChannel(chatChannelRecord) {
if (this.openChannels.includes(chatChannelRecord)) {
return;
}
this.openChannels.pushObject(chatChannelRecord);
this.rememberOpenedChannel(chatChannelRecord);
this.trigger('chat.opened', chatChannelRecord);
}

closeChannel(chatChannelRecord) {
const index = this.openChannels.findIndex((_) => _.id === chatChannelRecord.id);
if (index >= 0) {
this.openChannels.removeAt(index);
this.trigger('chat.closed', chatChannelRecord);
}
this.forgetOpenedChannel(chatChannelRecord);
}

rememberOpenedChannel(chatChannelRecord) {
let openedChats = this.appCache.get('open-chats', []);
if (isArray(openedChats) && !openedChats.includes(chatChannelRecord.id)) {
openedChats.pushObject(chatChannelRecord.id);
} else {
openedChats = [chatChannelRecord.id];
}
this.appCache.set('open-chats', openedChats);
}

forgetOpenedChannel(chatChannelRecord) {
let openedChats = this.appCache.get('open-chats', []);
if (isArray(openedChats)) {
openedChats.removeObject(chatChannelRecord.id);
} else {
openedChats = [];
}
this.appCache.set('open-chats', openedChats);
}

restoreOpenedChats() {
const openedChats = this.appCache.get('open-chats', []);
if (isArray(openedChats)) {
const findAll = openedChats.map((id) => this.store.findRecord('chat-channel', id));
return all(findAll).then((openedChatRecords) => {
if (isArray(openedChatRecords)) {
for (let i = 0; i < openedChatRecords.length; i++) {
const chatChannelRecord = openedChatRecords[i];
this.openChannel(chatChannelRecord);
}
}
return openedChatRecords;
});
}

return [];
}

getOpenChannels() {
return this.openChannels;
}

createChatChannel(name) {
const chatChannelRecord = this.store.createRecord('chat-channel', { name });
return chatChannelRecord.save().finally(() => {
this.trigger('chat.created', chatChannelRecord);
});
}

deleteChatChannel(chatChannelRecord) {
return chatChannelRecord.destroyRecord().finally(() => {
this.trigger('chat.deleted', chatChannelRecord);
});
}

updateChatChannel(chatChannelRecord, props = {}) {
chatChannelRecord.setProperties(props);
return chatChannelRecord.save().finally(() => {
this.trigger('chat.updated', chatChannelRecord);
});
}

addParticipant(chatChannelRecord, userRecord) {
const chatParticipant = this.store.createRecord('chat-participant', {
chat_channel_uuid: chatChannelRecord.id,
user_uuid: userRecord.id,
});
return chatParticipant.save().finally(() => {
this.trigger('chat.added_participant', chatParticipant, chatChannelRecord);
});
}

removeParticipant(chatChannelRecord, chatParticipant) {
return chatParticipant.destroyRecord().finally(() => {
this.trigger('chat.removed_participant', chatParticipant, chatChannelRecord);
});
}

async sendMessage(chatChannelRecord, senderRecord, messageContent = '', attachments = []) {
const chatMessage = this.store.createRecord('chat-message', {
chat_channel_uuid: chatChannelRecord.id,
sender_uuid: senderRecord.id,
content: messageContent,
attachment_files: attachments,
});

return chatMessage
.save()
.then((chatMessageRecord) => {
if (chatChannelRecord.doesntExistsInFeed('message', chatMessageRecord)) {
chatChannelRecord.feed.pushObject({
type: 'message',
created_at: chatMessageRecord.created_at,
data: chatMessageRecord.serialize(),
record: chatMessageRecord,
});
}
return chatMessageRecord;
})
.finally(() => {
this.trigger('chat.feed_updated', chatMessage, chatChannelRecord);
this.trigger('chat.message_created', chatMessage, chatChannelRecord);
});
}

deleteMessage(chatMessageRecord) {
return chatMessageRecord.destroyRecord().finally(() => {
this.trigger('chat.feed_updated', chatMessageRecord);
this.trigger('chat.message_deleted', chatMessageRecord);
});
}

insertChatMessageFromSocket(chatChannelRecord, data) {
// normalize and create record
const normalized = this.store.normalize('chat-message', data);
const record = this.store.push(normalized);

// make sure it doesn't exist in feed already
if (chatChannelRecord.existsInFeed('message', record)) {
return;
}

// create feed item
const item = {
type: 'message',
created_at: record.created_at,
data,
record,
};

// add item to feed
chatChannelRecord.feed.pushObject(item);

// trigger event
this.trigger('chat.feed_updated', record, chatChannelRecord);
this.trigger('chat.message_created', record, chatChannelRecord);
}

insertChatLogFromSocket(chatChannelRecord, data) {
// normalize and create record
const normalized = this.store.normalize('chat-log', data);
const record = this.store.push(normalized);

// make sure it doesn't exist in feed already
if (chatChannelRecord.existsInFeed('log', record)) {
return;
}

// create feed item
const item = {
type: 'log',
created_at: record.created_at,
data,
record,
};

// add item to feed
chatChannelRecord.feed.pushObject(item);

// trigger event
this.trigger('chat.feed_updated', record, chatChannelRecord);
this.trigger('chat.log_created', record, chatChannelRecord);
}

insertChatAttachmentFromSocket(chatChannelRecord, data) {
// normalize and create record
const normalized = this.store.normalize('chat-attachment', data);
const record = this.store.push(normalized);

// Find the chat message the record belongs to in the feed
const chatMessage = chatChannelRecord.feed.find((item) => {
return item.type === 'message' && item.record.id === record.chat_message_uuid;
});

// If we have the chat message then we can insert it to attachments
// This should work because chat message will always be created before the chat attachment
if (chatMessage) {
// Make sure the attachment isn't already attached to the message
const isNotAttached = chatMessage.record.attachments.find((attachment) => attachment.id === record.id) === undefined;
if (isNotAttached) {
chatMessage.record.attachments.pushObject(record);
// trigger event
this.trigger('chat.feed_updated', record, chatChannelRecord);
this.trigger('chat.attachment_created', record, chatChannelRecord);
}
}
}

insertChatReceiptFromSocket(chatChannelRecord, data) {
// normalize and create record
const normalized = this.store.normalize('chat-receipt', data);
const record = this.store.push(normalized);

// Find the chat message the record belongs to in the feed
const chatMessage = chatChannelRecord.feed.find((item) => {
return item.type === 'message' && item.record.id === record.chat_message_uuid;
});

// If we have the chat message then we can insert it to receipts
// This should work because chat message will always be created before the chat receipt
if (chatMessage) {
// Make sure the receipt isn't already attached to the message
const isNotAttached = chatMessage.record.receipts.find((receipt) => receipt.id === record.id) === undefined;
if (isNotAttached) {
chatMessage.record.receipts.pushObject(record);
// trigger event
this.trigger('chat.receipt_created', record, chatChannelRecord);
}
}
}

@task *loadMessages(chatChannelRecord) {
const messages = yield this.store.query('chat-message', { chat_channel_uuid: chatChannelRecord.id });
chatChannelRecord.set('messages', messages);
return messages;
}

@task *loadChannels(options = {}) {
const params = options.params || {};
const channels = yield this.store.query('chat-channel', params);
if (isArray(channels)) {
this.channels = channels;
}

if (typeof options.withChannels === 'function') {
options.withChannels(channels);
}

return channels;
}
}
39 changes: 23 additions & 16 deletions addon/services/socket.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { isBlank } from '@ember/utils';
import { later } from '@ember/runloop';
import toBoolean from '../utils/to-boolean';
import config from 'ember-get-config';

Expand Down Expand Up @@ -29,22 +30,28 @@ export default class SocketService extends Service {
}

async listen(channelId, callback) {
const channel = this.socket.subscribe(channelId);

// Track channel
this.channels.pushObject(channel);

// Listen to channel for events
await channel.listener('subscribe').once();

// Listen for channel subscription
(async () => {
for await (let output of channel) {
if (typeof callback === 'function') {
callback(output);
}
}
})();
later(
this,
async () => {
const channel = this.socket.subscribe(channelId);

// Track channel
this.channels.pushObject(channel);

// Listen to channel for events
await channel.listener('subscribe').once();

// Listen for channel subscription
(async () => {
for await (let output of channel) {
if (typeof callback === 'function') {
callback(output);
}
}
})();
},
300
);
}

closeChannels() {
Expand Down
7 changes: 4 additions & 3 deletions addon/utils/download.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,11 @@ export default function download(data, strFileName, strMimeType) {
anchor.className = 'download-js-link';
anchor.innerHTML = 'downloading...';
anchor.style.display = 'none';
anchor.addEventListener('click', function (e) {
function handleClick(e) {
e.stopPropagation();
this.removeEventListener('click', arguments.callee);
});
anchor.removeEventListener('click', handleClick);
}
anchor.addEventListener('click', handleClick);
document.body.appendChild(anchor);
later(
this,
Expand Down
1 change: 1 addition & 0 deletions app/services/chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/ember-core/services/chat';
12 changes: 12 additions & 0 deletions tests/unit/services/chat-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'dummy/tests/helpers';

module('Unit | Service | chat', function (hooks) {
setupTest(hooks);

// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let service = this.owner.lookup('service:chat');
assert.ok(service);
});
});

0 comments on commit f3d44cd

Please sign in to comment.