diff --git a/package-lock.json b/package-lock.json index d744781..1ae93a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3612,7 +3612,7 @@ "cssom": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==" + "integrity": "sha1-+FIGzuBO+oQfPFmCp0uparINZa0=" }, "cssstyle": { "version": "0.2.37", @@ -5007,8 +5007,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", @@ -5037,8 +5036,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", @@ -6813,7 +6811,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "integrity": "sha1-KBYjTiN4vdxOU1T6tcqold9xANk=", "optional": true }, "webidl-conversions": { @@ -8074,7 +8072,7 @@ "nwmatcher": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.4.4.tgz", - "integrity": "sha512-3iuY4N5dhgMpCUrOVnuAdGrgxVqV2cJpM+XNccjR2DKOB1RUP0aA+wGXEiNziG/UKboFyGBIoKOaNlJxx8bciQ==", + "integrity": "sha1-IoVjHzSpXw0Dlc2QDJbtObWPNG4=", "optional": true }, "nyc": { diff --git a/src/app/services/chat.service.ts b/src/app/services/chat.service.ts index 8aea172..5017061 100644 --- a/src/app/services/chat.service.ts +++ b/src/app/services/chat.service.ts @@ -1,14 +1,12 @@ -import { Injectable } from '@angular/core'; -import { Observable, of, BehaviorSubject, timer } from 'rxjs'; -import { mergeMap } from 'rxjs/operators'; +import {Injectable} from '@angular/core'; +import {BehaviorSubject, Observable, of} from 'rxjs'; -import { ChatMessage } from '../models/chat-message.model'; -import { RdfService } from './rdf.service'; -import { User } from '../models/user.model'; -import { ToastrService } from 'ngx-toastr'; +import {ChatMessage} from '../models/chat-message.model'; +import {RdfService} from './rdf.service'; +import {User} from '../models/user.model'; +import {ToastrService} from 'ngx-toastr'; import * as fileClient from 'solid-file-client'; -import { Message } from '@angular/compiler/src/i18n/i18n_ast'; @Injectable() export class ChatService { @@ -24,6 +22,12 @@ export class ChatService { friends: Array = new Array(); + /** + * First it will try to get the session and then to load the current user in session data. + * It will also load the user's friends. + * @param rdf Service to access and manage all the actions with SOLID. + * @param toastr To display error messages. + */ constructor(private rdf: RdfService, private toastr: ToastrService) { this.rdf.getSession(); this.loadUserData().then(response => { @@ -35,35 +39,54 @@ export class ChatService { // Observables + /** + * Returns current user in session as an Observable. + */ getUser() { return this.thisUser.asObservable(); } + /** + * Returns the other user (as an Observable) whose talking with the current user in session. + */ getOtherUser() { return of(this.otherUser); } + /** + * Returns all the current user friends. + */ getUsers(): Observable { return of(this.friends); } + /** + * Checks is there's a conversation opened. + */ isChatActive(): Observable { return this.isActive.asObservable(); } + /** + * Returns an observable of all the messages in a specific conversation. + */ getMessages(): Observable { return of(this.chatMessages); } // Loading methods + /** + * Waits for user to be in session, then finds its name, profile picture and webId to be used + * in the application. + */ private async loadUserData() { await this.rdf.getSession(); if (!this.rdf.session) { return; } const webId = this.rdf.session.webId; - let user : User = new User(webId, '', ''); + const user: User = new User(webId, '', ''); await this.rdf.getFieldAsStringFromProfile('fn').then(response => { user.username = response; }); @@ -74,6 +97,9 @@ export class ChatService { this.thisUser.next(user); } + /** + * Load current user's friends with their names and profile pictures sorted by username. + */ private async loadFriends() { await this.rdf.getSession(); if (!this.rdf.session) { @@ -87,6 +113,9 @@ export class ChatService { }); } + /** + * Load all messages from SOLID between the current user and the other user whose talking. + */ private async loadMessages() { console.log('Loading messages...'); if (!this.isActive) { @@ -94,12 +123,17 @@ export class ChatService { } await this.rdf.getSession(); this.chatMessages.length = 0; - this.loadMessagesFromTo(this.otherUser, this.thisUser.value); - this.loadMessagesFromTo(this.thisUser.value, this.otherUser); + await this.loadMessagesFromTo(this.otherUser, this.thisUser.value); + await this.loadMessagesFromTo(this.thisUser.value, this.otherUser); } + /** + * Auxiliary method to load messages with all their details (date, text, sender) between user1 and user2. + * @param user1 First pair of the communication. + * @param user2 Second pair of the communication. + */ private async loadMessagesFromTo(user1: User, user2: User) { - const messages = (await this.rdf.getElementsFromContainer(await this.getChatUrl(user1, user2))) + const messages = (await this.rdf.getElementsFromContainer(await this.getChatUrl(user1, user2))); if (!messages) { this.toastr.error('Please make sure the other user has clicked on your chat', 'Could not load messages'); this.isActive.next(false); @@ -113,7 +147,7 @@ export class ChatService { const text = this.rdf.getValueFromSchema('text', url); const date = Date.parse(this.rdf.getValueFromSchema('dateSent', url)); const name = await this.rdf.getFriendData(sender, 'fn'); - //console.log('Messages loaded: ' + messages); + // console.log('Messages loaded: ' + messages); this.addMessage(new ChatMessage(name, text, date)); }); } @@ -121,32 +155,45 @@ export class ChatService { // Message methods /** - * Gets the URL for the chat resource location - * @param user1 user who sends - * @param user2 user who recieves + * Gets the URL for the chat resource location. + * @param user1 User who sends. + * @param user2 User who receives. */ private async getChatUrl(user1: User, user2: User): Promise { await this.rdf.getSession(); const root = user1.webId.replace('/profile/card#me', '/private/dechat/chat_'); const name = user2.webId.split('/')[2].split('.')[0]; - const finalUrl = root + name + '/'; - // console.log(finalUrl); - return finalUrl; + // console.log(root + name + '/'); + return root + name + '/'; } - private sortByDateDesc(m1: ChatMessage, m2: ChatMessage) { - return m1.timeSent > m2.timeSent ? 1 : m1.timeSent < m2.timeSent ? -1 : 0; - } + /** + * Auxiliary comparator method to sort messages by their date. + * @param m1 A message. + * @param m2 Another message. + */ + private sortByDateDesc = (m1: ChatMessage, m2: ChatMessage) => m1.timeSent > m2.timeSent ? 1 : m1.timeSent < m2.timeSent ? -1 : 0; - private sortUserByName(u1: User, u2: User) { - return u1.username.localeCompare(u2.username); - } + /** + * Auxiliary comparator method to sort users by their name. + * @param u1 An user. + * @param u2 Another user. + */ + private sortUserByName = (u1: User, u2: User) => u1.username.localeCompare(u2.username); + /** + * Method to add a message to the ones present in the system. + * @param message Message to be added. + */ private addMessage(message: ChatMessage) { this.chatMessages.push(message); this.chatMessages.sort(this.sortByDateDesc); } + /** + * Method to send a message to SOLID. + * @param msg Text of the message to be sent. + */ async sendMessage(msg: string) { if (msg !== '' && this.otherUser) { const newMsg = new ChatMessage(this.thisUser.value.username, msg); @@ -155,6 +202,10 @@ export class ChatService { } } + /** + * Method that declares the structure of the message in XML and sends it to SOLID. + * @param msg Instance of the message to be sent. + */ private async postMessage(msg: ChatMessage) { const message = ` @prefix : <#>. @@ -169,12 +220,16 @@ export class ChatService { `; const path = await this.getChatUrl(this.thisUser.value, this.otherUser) + 'message.ttl'; fileClient.createFile(path).then((fileCreated: any) => { - fileClient.updateFile(fileCreated, message).then( success => { - console.log('Message has been sended succesfully'); - }, (err: any) => console.log(err) ); + fileClient.updateFile(fileCreated, message).then(success => { + console.log('Message has been sent successfully'); + }, (err: any) => console.log(err)); }); } + /** + * Method invoked we switch to another conversation with a different user. + * @param user The new user we are talking to. + */ async changeChat(user: User) { this.isActive.next(true); this.otherUser = user; @@ -185,12 +240,20 @@ export class ChatService { // Solid methods + /** + * Adds a friend to your SOLID profile given its webId. + * @param webId Of the user that we are adding. + */ addFriend(webId: string) { if (this.thisUser.value.webId !== webId) { this.rdf.addFriend(webId); } } + /** + * Removes a friend to your SOLID profile given its webId and deletes all the stored conversations. + * @param webId Of the user that we are removing. + */ removeFriend(webId: string) { const name = webId.split('/')[2].split('.')[0]; if (this.thisUser.value.webId !== webId) { @@ -201,12 +264,22 @@ export class ChatService { } } + /** + * Removes the conversation (its folder structure in the application) with a user + * given a path. + * @param path Of the folder to be deleted. + */ private async removeFolderStructure(path: string) { fileClient.deleteFolder(path).then((success: any) => { console.log(`Removed folder ${path}.`); }, (err: any) => console.log(err)); } + /** + * Method that evaluates if its possible to chat with another user (it has the correct folder + * structure) and if it's not possible it will create the appropriate folder structure and give + * the permissions. + */ async checkFolderStructure() { await this.rdf.getSession(); try { @@ -214,11 +287,11 @@ export class ChatService { fileClient.readFolder(charUrl).then((success: any) => { console.log('Folder structure correct'); }, (err: any) => { - console.log('Attempting to create: ' + charUrl); - this.createFolderStructure(charUrl).then(res => { - console.log('Creating ACL file...'); - this.grantAccessToFolder(charUrl, this.otherUser); - }); + console.log('Attempting to create: ' + charUrl); + this.createFolderStructure(charUrl).then(res => { + console.log('Creating ACL file...'); + this.grantAccessToFolder(charUrl, this.otherUser); + }); }); }); } catch (error) { @@ -226,16 +299,25 @@ export class ChatService { } } + /** + * Auxiliary method to create a folder structure given its path. + * @param path To the folder to be created. + */ private async createFolderStructure(path: String) { await fileClient.createFolder(path).then((success: any) => { console.log(`Created folder ${path}.`); }, (err: string) => console.log('Could not create folder structure: ' + err)); } + /** + * Method that creates the ACL structure to grant VIEW permissions to a user to a folder. + * @param path To the folder whose permissions we are going to modify. + * @param user To grant permissions. + */ private grantAccessToFolder(path: string | String, user: User) { const webId = user.webId.replace('#me', '#'); const acl = - `@prefix : <#>. + `@prefix : <#>. @prefix n0: . @prefix ch: <./>. @prefix c: . @@ -258,4 +340,4 @@ export class ChatService { console.log('Folder permisions added'); }, (err: string) => console.log('Could not set folder permisions' + err)); } -} \ No newline at end of file +} diff --git a/src/app/services/rdf.service.ts b/src/app/services/rdf.service.ts index f3923b4..ce7bb80 100644 --- a/src/app/services/rdf.service.ts +++ b/src/app/services/rdf.service.ts @@ -1,17 +1,16 @@ -import { Injectable } from '@angular/core'; -import { SolidSession } from '../models/solid-session.model'; +import {Injectable} from '@angular/core'; +import {SolidSession} from '../models/solid-session.model'; +// TODO: Remove any UI interaction from this service +import {ChatMessage} from '../models/chat-message.model'; +import {NgForm} from '@angular/forms'; +import {ToastrService} from 'ngx-toastr'; +import {NamedNode} from 'src/assets/types/rdflib'; + declare let solid: any; declare let $rdf: any; // import * as $rdf from 'rdflib' -// TODO: Remove any UI interaction from this service -import { ChatMessage } from '../models/chat-message.model'; -import { NgForm } from '@angular/forms'; -import { ToastrService } from 'ngx-toastr'; -import { fetcher, NamedNode } from 'src/assets/types/rdflib'; -import { of } from 'rxjs'; - const VCARD = $rdf.Namespace('http://www.w3.org/2006/vcard/ns#'); const FOAF = $rdf.Namespace('http://xmlns.com/foaf/0.1/'); const LDP = $rdf.Namespace('http://www.w3.org/ns/ldp#'); @@ -73,7 +72,7 @@ export class RdfService { */ getValueFromVcard = (node: string, webId?: string): string | any => { return this.getValueFromNamespace(node, VCARD, webId); - }; + } /** * Gets a node that matches the specified pattern using the FOAF onthology @@ -83,7 +82,7 @@ export class RdfService { */ getValueFromFoaf = (node: string, webId?: string) => { return this.getValueFromNamespace(node, FOAF, webId); - }; + } /** * Gets a node that matches the specified pattern using the FOAF onthology @@ -93,8 +92,14 @@ export class RdfService { */ getValueFromSchema = (node: string, webId?: string) => { return this.getValueFromNamespace(node, SCHEMA, webId); - }; + } + /** + * Method that transforms the data from a form to update the profile. + * @param form With the new values. + * @param me Current user in session. + * @param doc + */ transformDataForm = (form: NgForm, me: any, doc: any) => { const insertions = []; const deletions = []; @@ -135,13 +140,21 @@ export class RdfService { } } }); - return { insertions: insertions, deletions: deletions }; } + // TODO: improve this JSDoc + /** + * Method that adds a new linked field. + * @param insertions + * @param predicate + * @param fieldValue + * @param why + * @param me + */ private addNewLinkedField(field: string, insertions: any[], predicate: any, fieldValue: any, why: any, me: any) { // Generate a new ID. This id can be anything but needs to be unique. const newId = field + ':' + Date.now(); @@ -164,20 +177,25 @@ export class RdfService { insertions.push($rdf.st(me, newPredicate, newSubject, why)); } + /** + * Returns an URI for a given resource. + * @param field To be transformed. + * @param me Default URI. + */ private getUriForField(field, me): string { let uriString: string; let uri: any; - switch(field) { + switch (field) { case 'phone': uriString = this.getValueFromVcard('hasTelephone'); - if(uriString) { + if (uriString) { uri = $rdf.sym(uriString); } break; case 'email': uriString = this.getValueFromVcard('hasEmail'); - if(uriString) { + if (uriString) { uri = $rdf.sym(uriString); } break; @@ -217,6 +235,11 @@ export class RdfService { return fieldValue; } + /** + * Returns the value of a field of the old profile (before update). + * @param field To be accessed. + * @param oldProfile To be accessed. + */ private getOldFieldValue(field: string, oldProfile: { [x: string]: any; }): any { let oldValue: any; @@ -235,10 +258,13 @@ export class RdfService { oldValue = oldProfile[field]; break; } - return oldValue; } + /** + * Auxiliary method to return the name of field. + * @param field To get the name from. + */ private getFieldName(field: string): string { switch (field) { case 'company': @@ -251,6 +277,10 @@ export class RdfService { } } + /** + * Method that uses the update manager to, given the data of a form, update the info in your profile. + * @param form Form with the new profile data. + */ updateProfile = async (form: NgForm) => { const me = $rdf.sym(this.session.webId); const doc = $rdf.NamedNode.fromValue(this.session.webId.split('#')[0]); @@ -270,6 +300,9 @@ export class RdfService { } } + /** + * Function to get address. + */ getAddress = () => { const linkedUri = this.getValueFromVcard('hasAddress'); @@ -281,32 +314,36 @@ export class RdfService { street: this.getValueFromVcard('street-address', linkedUri), }; } - return {}; } - // Function to get email. This returns only the first email, which is temporary + /** + * Function to get email. This returns only the first email, which is temporary + */ getEmail = () => { const linkedUri = this.getValueFromVcard('hasEmail'); if (linkedUri) { return this.getValueFromVcard('value', linkedUri).split('mailto:')[1]; } - return ''; } - // Function to get phone number. This returns only the first phone number, which is temporary. It also ignores the type. + /** + * Function to get phone number. This returns only the first phone number, which is temporary. It also ignores the type. + */ getPhone = () => { const linkedUri = this.getValueFromVcard('hasTelephone'); - if(linkedUri) { + if (linkedUri) { return this.getValueFromVcard('value', linkedUri).split('tel:+')[1]; } - }; + } + /** + * Returns the whole profile of the current user in session. + */ getProfile = async () => { - if (!this.session) { await this.getSession(); } @@ -326,13 +363,13 @@ export class RdfService { } catch (error) { console.log(`Error fetching data: ${error}`); } - }; + } /** * Gets any resource that matches the node, using the provided Namespace - * @param {string} node The name of the predicate to be applied using the provided Namespace + * @param {string} node The name of the predicate to be applied using the provided Namespace * @param {$rdf.namespace} namespace The RDF Namespace - * @param {string?} webId The webId URL (e.g. https://yourpod.solid.community/profile/card#me) + * @param {string?} webId The webId URL (e.g. https://yourpod.solid.community/profile/card#me) */ private getValueFromNamespace(node: string, namespace: any, webId?: string): string | any { const store = this.store.any($rdf.sym(webId || this.session.webId), namespace(node)); @@ -346,10 +383,21 @@ export class RdfService { // EXTRA FUNCTIONS // //////////////////// + /** + * Method to extract a value from the profile and return it as string. + * @param field To be accessed. + */ async getFieldAsStringFromProfile(field: string): Promise { return this.getFieldAsString(this.session.webId, field, VCARD); } + /** + * Generic method to extract a value as a string given a field to be accessed and the namespace where we + * are working. + * @param webId Where the resource is located. + * @param field To be accessed. + * @param namespace To work with. + */ private async getFieldAsString(webId: string, field: string, namespace: any): Promise { try { await this.fetcher.load(this.store.sym(webId).doc()); @@ -359,6 +407,13 @@ export class RdfService { } } + /** + * Generic method to extract a value as and array given a field to be accessed and the namespace where we + * are working. + * @param webId Where the resource is located. + * @param field To be accessed. + * @param namespace To work with. + */ private async getDataAsArray(webId: string | String, field: string, namespace: any): Promise> { try { await this.fetcher.load(this.store.sym(webId).doc()); @@ -369,25 +424,41 @@ export class RdfService { } } + /** + * Returns and array containing the nodes representing the friends of the current user. + */ async getFriends(): Promise> { const webId = this.session.webId; return this.getDataAsArray(webId, 'knows', FOAF); } + /** + * Returns a value from a given field provided a webId. + * @param webId In this case we are going to use the webId of our contacts. + * @param field To be accessed. + */ async getFriendData(webId: any, field: string): Promise { return this.getFieldAsString(webId, field, VCARD); } + /** + * Returns an array of nodes from the container specified. + * @param container To be accessed. + */ async getElementsFromContainer(container: String): Promise> { return this.getDataAsArray(container, 'contains', LDP); } + /** + * Method to add a friend to the current user's friend list given its webId. + * @param webId Of the friend to be added. + */ addFriend(webId: string) { const me = $rdf.sym(this.session.webId); const friend = $rdf.sym(webId); const toBeInserted = $rdf.st(me, FOAF('knows'), friend, me.doc()); this.updateManager.update([], toBeInserted, (response, success, message) => { - if(success) { + if (success) { this.toastr.success('Friend added', 'Success!'); } else { this.toastr.error('Message: ' + message, 'An error has occurred'); @@ -395,6 +466,10 @@ export class RdfService { }); } + /** + * Method to remove a friend from the current user's friend list given its webId. + * @param webId Of the friend to be removed. + */ removeFriend(webId: string) { const me = $rdf.sym(this.session.webId); const friend = $rdf.sym(webId); @@ -410,16 +485,15 @@ export class RdfService { // TODO: when creating a message, use an autogenerated UID based on the timestamp for the name /** - * Posts a message to a container - * @param message - * @param webId + * Posts a message to a container. + * @param message To be posted. + * @param webId Where the message is going to be placed. */ - async postMessage(message: ChatMessage, webId: any) { + postMessage = async (message: ChatMessage, webId: any) => { const acl = $rdf.graph(); acl.add(); $rdf.serialize(null, acl, webId, (err: any, body: any) => { // use something like updateManager to PUT the body }); } - }