From 1a0e02bdeff8d4a276ea87111fa54c46f163ed8a Mon Sep 17 00:00:00 2001 From: emkartal1 Date: Wed, 8 Nov 2023 19:16:11 +0100 Subject: [PATCH] Add a new ServiceSpotify, improved the structure and added new routes to modify user information and favorite music :white_check_mark: --- src/Api/src/assets/images/default_user.png | Bin 0 -> 8833 bytes src/Api/src/controllers/spotifyController.ts | 6 +- src/Api/src/controllers/userController.ts | 127 +- src/Api/src/database/UserSchema.ts | 8 + src/Api/src/middlewares/UserValidation.ts | 2 +- src/Api/src/models/Music.ts | 5 + src/Api/src/models/User.ts | 9 +- src/Api/src/services/LocationService.ts | 8 +- src/Api/src/services/UserService.ts | 63 +- src/FLAD/assets/images/confirm_icon.png | Bin 0 -> 1059 bytes src/FLAD/components/AdjustSize.tsx | 8 - src/FLAD/components/AnimatedParalax.tsx | 44 - src/FLAD/components/Artist.tsx | 71 - src/FLAD/components/ArtistChip.tsx | 38 - src/FLAD/components/CardMusicComponent.tsx | 4 +- src/FLAD/components/CircularProgressBar.tsx | 34 - src/FLAD/components/FriendComponent.tsx | 4 +- src/FLAD/components/HalfCircle.tsx | 20 - ...stComponent.tsx => HorizontalFlatList.tsx} | 2 +- src/FLAD/components/LoadingComponent.tsx | 2 +- src/FLAD/components/PaginatorComponent.tsx | 2 +- src/FLAD/components/littleCard.tsx | 31 + src/FLAD/data/data.ts | 54 - src/FLAD/lib/index.js | 7 - src/FLAD/lib/mqtt.js | 2395 ----------------- src/FLAD/lib/storage.js | 10 - src/FLAD/models/Artist.ts | 42 +- src/FLAD/models/Music.ts | 91 +- src/FLAD/models/MusicServiceProvider.ts | 15 + src/FLAD/models/Person.ts | 19 + src/FLAD/models/Spot.ts | 21 +- src/FLAD/models/mapper/ArtistMapper.ts | 7 + src/FLAD/models/mapper/MusicMapper.ts | 8 +- src/FLAD/models/mapper/SpotMapper.ts | 7 + src/FLAD/navigation/AuthNavigation.tsx | 10 +- src/FLAD/navigation/FavoriteNavigation.tsx | 6 +- src/FLAD/navigation/HomeNavigation.tsx | 137 +- src/FLAD/navigation/MessagingNavigation.tsx | 10 +- src/FLAD/navigation/SpotNavigation.tsx | 14 +- src/FLAD/package.json | 1 - src/FLAD/redux/actions/appActions.ts | 13 +- src/FLAD/redux/actions/spotActions.tsx | 9 +- src/FLAD/redux/actions/userActions.tsx | 43 +- src/FLAD/redux/reducers/appReducer.tsx | 14 +- src/FLAD/redux/reducers/userReducer.tsx | 31 +- src/FLAD/redux/store.tsx | 2 +- src/FLAD/redux/thunk/appThunk.tsx | 81 + src/FLAD/redux/thunk/authThunk.tsx | 31 +- src/FLAD/redux/thunk/socialThunk.tsx | 17 - src/FLAD/redux/thunk/spotThunk.tsx | 64 +- src/FLAD/redux/thunk/userThunk.tsx | 66 +- src/FLAD/redux/types/playlistTypes.tsx | 2 - src/FLAD/redux/types/spotTypes.tsx | 2 +- src/FLAD/redux/types/spotifyTypes.ts | 2 +- src/FLAD/redux/types/userTypes.tsx | 14 +- src/FLAD/screens/ChatScreen.tsx | 30 +- src/FLAD/screens/ConversationScreen.tsx | 6 +- src/FLAD/screens/DetailScreen.tsx | 72 +- src/FLAD/screens/FavoriteScreen.tsx | 69 +- src/FLAD/screens/LoginScreen.tsx | 3 +- src/FLAD/screens/ProfilScreen.tsx | 133 +- src/FLAD/screens/RegisterScreen.tsx | 21 +- src/FLAD/screens/SettingScreen.tsx | 29 +- src/FLAD/screens/SpotScreen.tsx | 19 +- src/FLAD/services/EmptyMusicService.ts | 26 + .../musics/interfaces/IMusicService.ts | 11 + .../services/musics/spotify/SpotifyService.ts | 189 ++ .../services/musics/spotify/TokenSpotify.ts | 41 + .../services/spotify/IspotifyAuthHandler.ts | 4 - src/FLAD/services/spotify/spotify.service.ts | 156 -- .../spotifyRequestHandler/IspotifyService.ts | 3 - .../spotify/spotifyRequestHandler/utils.tsx | 78 - src/FLAD/utils/MqttClient.js | 80 - 73 files changed, 1254 insertions(+), 3449 deletions(-) create mode 100644 src/Api/src/assets/images/default_user.png create mode 100644 src/Api/src/models/Music.ts create mode 100644 src/FLAD/assets/images/confirm_icon.png delete mode 100644 src/FLAD/components/AdjustSize.tsx delete mode 100644 src/FLAD/components/AnimatedParalax.tsx delete mode 100644 src/FLAD/components/Artist.tsx delete mode 100644 src/FLAD/components/ArtistChip.tsx delete mode 100644 src/FLAD/components/CircularProgressBar.tsx delete mode 100644 src/FLAD/components/HalfCircle.tsx rename src/FLAD/components/{HorizontalFlatListComponent.tsx => HorizontalFlatList.tsx} (87%) create mode 100644 src/FLAD/components/littleCard.tsx delete mode 100644 src/FLAD/data/data.ts delete mode 100644 src/FLAD/lib/index.js delete mode 100644 src/FLAD/lib/mqtt.js delete mode 100644 src/FLAD/lib/storage.js create mode 100644 src/FLAD/models/MusicServiceProvider.ts create mode 100644 src/FLAD/models/Person.ts create mode 100644 src/FLAD/models/mapper/ArtistMapper.ts create mode 100644 src/FLAD/models/mapper/SpotMapper.ts create mode 100644 src/FLAD/redux/thunk/appThunk.tsx delete mode 100644 src/FLAD/redux/thunk/socialThunk.tsx create mode 100644 src/FLAD/services/EmptyMusicService.ts create mode 100644 src/FLAD/services/musics/interfaces/IMusicService.ts create mode 100644 src/FLAD/services/musics/spotify/SpotifyService.ts create mode 100644 src/FLAD/services/musics/spotify/TokenSpotify.ts delete mode 100644 src/FLAD/services/spotify/IspotifyAuthHandler.ts delete mode 100644 src/FLAD/services/spotify/spotify.service.ts delete mode 100644 src/FLAD/services/spotify/spotifyRequestHandler/IspotifyService.ts delete mode 100644 src/FLAD/services/spotify/spotifyRequestHandler/utils.tsx delete mode 100644 src/FLAD/utils/MqttClient.js diff --git a/src/Api/src/assets/images/default_user.png b/src/Api/src/assets/images/default_user.png new file mode 100644 index 0000000000000000000000000000000000000000..41b21ffa8ff10c8cd359c92b474d672605b4f3ad GIT binary patch literal 8833 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 { res.status(400).send("Cannot get a new refresh token"); @@ -108,7 +108,7 @@ class SpotifyController implements IController { json: true }; try { - var resp = await axios(authOptions); + const resp = await axios(authOptions); if (resp.status === 200) { let access_token = resp.data.access_token; let expiration = resp.data.expires_in; diff --git a/src/Api/src/controllers/userController.ts b/src/Api/src/controllers/userController.ts index 3f7ede1..55614d4 100644 --- a/src/Api/src/controllers/userController.ts +++ b/src/Api/src/controllers/userController.ts @@ -1,4 +1,4 @@ -import { Router, Request, Response, NextFunction, RequestHandler } from 'express'; +import { Router, Request, Response, NextFunction } from 'express'; import IController from './interfaces/IController'; import HttpException from '../exception/HttpException'; import User from '../models/User'; @@ -7,7 +7,10 @@ import validator from '../middlewares/UserValidation' import validationMiddleware from '../middlewares/validationMiddleware'; import authenticator from '../middlewares/authMiddleware' import LocationService from '../services/LocationService'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; +import { IMusic } from '../models/Music'; +import * as fs from 'fs'; +import * as base64js from 'base64-js'; class UserController implements IController { public path = '/user'; @@ -34,6 +37,12 @@ class UserController implements IController { this.router.get(`${this.path}`, authenticator, this.getUser); this.router.delete(`${this.path}`, authenticator, this.deleteUser); this.router.get(`${this.path}/nextTo`, authenticator, this.getUserNext); + this.router.delete(`${this.path}/musics/:id`, authenticator, this.deleteMusic); + this.router.post(`${this.path}/musics`, authenticator, this.addMusic); + this.router.get(`${this.path}/musics`, authenticator, this.getMusics); + this.router.put(`${this.path}/name`, authenticator, this.setName); + this.router.put(`${this.path}/email`, authenticator, this.setEmail); + } private register = async ( @@ -49,7 +58,7 @@ class UserController implements IController { const apiBaseUrl = process.env.API_BASE_URL || 'http://localhost:8080/api'; const refreshUrl = `${apiBaseUrl}/spotify/refresh?refresh_token=${tokenSpotify}`; try { - var authOptions = { + const authOptions = { method: 'GET', url: refreshUrl, json: true @@ -69,11 +78,15 @@ class UserController implements IController { image = images[0].url; } else { - image = "data:image/png;base64,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" + const imagePath = './src/assets/images/default_user.png'; + const imageBuffer = fs.readFileSync(imagePath); + const base64Image = 'data:image/png;base64,' + base64js.fromByteArray(imageBuffer); + image = base64Image } } } } catch (error: any) { + console.log(error); if (error.response.status === 400) { res.status(401).send("Unauthorized: Spotify token is invalid"); return; @@ -154,6 +167,112 @@ class UserController implements IController { next(new HttpException(400, 'Cannot create get netUser: ' + error.message)); } } + + private deleteMusic = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const musicId: string = req.params.id; + if (!musicId) { + return res.status(400).json({ error: 'musicId are required fields.' }); + } + + const deleted = await this.userService.deleteMusic(_id, musicId); + + if (deleted) { + res.status(200).send({ message: 'Music deleted successfully.' }); + } else { + res.status(404).json({ error: 'Music not found.' }); + } + + } catch (error: any) { + next(new HttpException(404, error.message)); + } + } + + private addMusic = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const { idMusic, idUser } = req.body; + if (!idMusic || !idUser) { + return res.status(400).json({ error: 'idMusic and idUser are required fields.' }); + } + const music: IMusic = { + idMusic, + idUser, + date: new Date(), + }; + await this.userService.addMusic(_id, music); + res.status(201).send({ music }); + } catch (error: any) { + next(new HttpException(400, error.message)); + } + } + + private getMusics = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const userId: string = req.user.id; + const musics = await this.userService.getMusics(userId); + return res.status(200).json({ musics }); + } catch (error: any) { + next(new HttpException(400, error.message)); + } + } + + private setName = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const { name } = req.body; + + const regex = /^\w+$/; + if (!regex.test(name) || !name) { + return res.status(400).json({ error: "Name should only contain alphanumeric characters (letters, numbers, and underscores)" }); + } + + await this.userService.setName(_id, name.toLowerCase()); + + res.status(200).json({ message: 'Name updated successfully' }); + } catch (error: any) { + next(new HttpException(409, error.message)); + } + } + + private setEmail = async ( + req: Request, + res: Response, + next: NextFunction + ): Promise => { + try { + const { _id } = req.user; + const { email } = req.body; + + const regex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + if (!regex.test(email) || !email) { + return res.status(400).json({ error: "Invalid email" }); + } + + await this.userService.setEmail(_id, email.toLowerCase()); + + res.status(200).json({ message: 'Email updated successfully' }); + } catch (error: any) { + next(new HttpException(409, error.message)); + } + } } export default UserController; diff --git a/src/Api/src/database/UserSchema.ts b/src/Api/src/database/UserSchema.ts index 447f71b..8ea93a4 100644 --- a/src/Api/src/database/UserSchema.ts +++ b/src/Api/src/database/UserSchema.ts @@ -31,6 +31,14 @@ const userSchema = new Schema({ image: { type: String, required: true + }, + musics_likes: { + type: [{ + idMusic: String, + idUser: String, + date: Date + }], + default: [] } }, { timestamps: true } diff --git a/src/Api/src/middlewares/UserValidation.ts b/src/Api/src/middlewares/UserValidation.ts index 256a289..f900307 100644 --- a/src/Api/src/middlewares/UserValidation.ts +++ b/src/Api/src/middlewares/UserValidation.ts @@ -1,7 +1,7 @@ import Joi from 'joi'; const register = Joi.object({ - name: Joi.string().max(30).required().regex(/^[a-zA-Z0-9_]+$/) + name: Joi.string().max(30).required().regex(/^\w+$/) .message("Name should only contain alphanumeric characters (letters, numbers, and underscores)"), email: Joi.string().email().required(), password: Joi.string().min(6).required(), diff --git a/src/Api/src/models/Music.ts b/src/Api/src/models/Music.ts new file mode 100644 index 0000000..6146620 --- /dev/null +++ b/src/Api/src/models/Music.ts @@ -0,0 +1,5 @@ +export interface IMusic { + idMusic: string; + idUser: string; + date: Date; +} \ No newline at end of file diff --git a/src/Api/src/models/User.ts b/src/Api/src/models/User.ts index effcfbd..1065844 100644 --- a/src/Api/src/models/User.ts +++ b/src/Api/src/models/User.ts @@ -1,10 +1,13 @@ import { Document } from 'mongoose'; +import { IMusic } from './Music'; export default interface User extends Document { - email: string; + idSpotify: string; + tokenSpotify: string; name: string; + email: string; password: string; - idFlad: string; - idSpotify: string; isValidPassword(password: string): Promise; + image: string; + musics_likes: IMusic[]; } \ No newline at end of file diff --git a/src/Api/src/services/LocationService.ts b/src/Api/src/services/LocationService.ts index b148a98..28c05c2 100644 --- a/src/Api/src/services/LocationService.ts +++ b/src/Api/src/services/LocationService.ts @@ -35,10 +35,10 @@ class LocationService { return 0; } else { - var radlat1 = Math.PI * lat1 / 180; - var radlat2 = Math.PI * lat2 / 180; - var theta = lon1 - lon2; - var radtheta = Math.PI * theta / 180; + const radlat1 = Math.PI * lat1 / 180; + const radlat2 = Math.PI * lat2 / 180; + const theta = lon1 - lon2; + const radtheta = Math.PI * theta / 180; var dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta); if (dist > 1) { diff --git a/src/Api/src/services/UserService.ts b/src/Api/src/services/UserService.ts index d853301..8554155 100644 --- a/src/Api/src/services/UserService.ts +++ b/src/Api/src/services/UserService.ts @@ -1,3 +1,4 @@ +import { IMusic } from "../models/Music"; import LocationSchema from "../database/LocationSchema"; import UserSchema from "../database/UserSchema"; import token from "./TokenService"; @@ -46,15 +47,73 @@ class UserService { public async delete( id: string - ): Promise { + ): Promise { try { await this.user.findByIdAndRemove(id); await this.location.findByIdAndRemove(id); - return; } catch (error: any) { throw new Error(error.message); } } + + public async addMusic(userId: string, music: IMusic): Promise { + try { + return await this.user.findByIdAndUpdate(userId, { + $push: { musics_likes: music }, + }); + } catch (error: any) { + throw new Error(error.message); + } + } + + public async deleteMusic(userId: string, musicId: string): Promise { + try { + const userOld = await this.user.findById(userId); + const userNew = await this.user.findByIdAndUpdate(userId, { + $pull: { musics_likes: { _id: musicId } }, + }, { new: true }); + + if (userOld.musics_likes.length === userNew.musics_likes.length) { + return false; + } + return true; + } catch (error) { + throw new Error(error.message); + } + } + + public async getMusics(userId: string): Promise { + try { + const user = await this.user.findById(userId); + return user?.musics_likes || []; + } catch (error) { + throw new Error(error.message); + } + } + + public async setName(userId: string, newName: string): Promise { + try { + await this.user.findByIdAndUpdate( + userId, + { name: newName }, + { new: true } + ); + } catch (error) { + throw new Error(error.message); + } + } + + public async setEmail(userId: string, newEmail: string): Promise { + try { + await this.user.findByIdAndUpdate( + userId, + { email: newEmail }, + { new: true } + ); + } catch (error) { + throw new Error(error.message); + } + } } export default UserService; \ No newline at end of file diff --git a/src/FLAD/assets/images/confirm_icon.png b/src/FLAD/assets/images/confirm_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..04fd41195a819dc073454eff165bf8443d5c8e18 GIT binary patch literal 1059 zcmV+;1l;?HP)0;$9l5d^DXnzi!-S#0wGKVXryGFYr@8#ZXP+$Ipq6cHj; zfizm`BJtB@kwD_^zP-Nt^r`I5&dj=*_sbjz`7n=U&fa_H&TIz2U@#aA219~j&;^6j zyMqv8%7wl%bU{aQ2BvY~aBqFeduq)4c9?zy@n;Pixa8XI+PC@6WGTxzcDFuZGkgZg z5YhqzR<0w=e0}UJt3RW_>3h|w2CxF80Lx;xK0R`lRK`l>9KF9g=%Div(nDqEhci?u zu!?M;B^zL4&m5!+uWnYSRR&rL1I8d_FlOIW87Gu;;!brK0S6&v(42-I;2GICp&W)R zkt>r5HMc$_8zYp1EPYWEfUbOQ(wcLSF>(YLIRcCv0Y;7h2NK4~BLMk~4X3=%Z$BX9 zR~xXk4O&Q?oYHN_7er)6nD;e;GIcoh86VTt+)p?ze#K<8j*^8}z?)xtY zx=4(i*~_g_L$xNqswbt0M99gESpG*dI*T!p2ssor&Aju{skG4moy1`Q&= z6cB4UB!w3>Pc)}fK&<6Zv)9b8BCI!^0%9wN3h;%qtvyT_=@bx4IqlM^w1Ivh4hwB^ z`VWEM+W!wx^fWxQ?5!vza^sdSh$fX;s4ExH^T$~vJq-^nkwdRMi6JNgiq~!BXL(~O zopkp!Joa4<$!JaqiiWvy`LE*jG~f5aPp5!=mqVe?3xC=)^3o-sT@hYf4?mp(w4_zL z<(BFYuiGVfV#4fccxaJB%k9IJ)=5zvqQk179tE`9SW+R65^YO)@lC@|iDoEra^ruC zE{TeAiy%U46PSC|@XvXhq2tZT-v?JSZF-vGdUWX&puIz$ExFtXp!##=oCVoYeosTI zRMcpfLt>~v^Cnxo!QUc8(bMnhv5ut57DH;|fbtE5yBK$9*?#kO0 zXd;Yb&rS*b$RhiqwLnun3iF&smC)so2x{)A@NR9RA4xo6aUhbngGmG}@o~remxTZ$ zM}Uzd;K0gZHY>}<2<2GJlHLzUGeS8vtF{R#WoOTk&P^zXE^i^o!!K!gJ^JRp+?rwN zD&0*=6bzrL43o-ns=b9YMGwv8^L+1fm9bJeWiR|(R=bo2K{N-Ni|Tl#rJQPyF9)ZPF9 literal 0 HcmV?d00001 diff --git a/src/FLAD/components/AdjustSize.tsx b/src/FLAD/components/AdjustSize.tsx deleted file mode 100644 index 32ae5ed..0000000 --- a/src/FLAD/components/AdjustSize.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default function AdjustSize(Text: string) { - const titleLength = Text.length; - const minFontSize = 23; - const maxFontSize = 48; - const fontRatio = 1.1; - const fontSize = Math.max(minFontSize, maxFontSize - (titleLength * fontRatio)); - return fontSize; -} \ No newline at end of file diff --git a/src/FLAD/components/AnimatedParalax.tsx b/src/FLAD/components/AnimatedParalax.tsx deleted file mode 100644 index bba9f4a..0000000 --- a/src/FLAD/components/AnimatedParalax.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { View } from "react-native"; -import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle } from "react-native-reanimated"; - -const halfPi = Math.PI / 2; - -export default function AnimatedParalax() { - const sensor = useAnimatedSensor(SensorType.ROTATION); - const styleAniamatedImage = useAnimatedStyle(() => { - const { pitch, roll } = sensor.sensor.value; - const verticalAxis = interpolate( - pitch, - [-halfPi, halfPi], - [-25, 25] - ) - const horizontalAxis = interpolate( - roll, - [-halfPi * 2, halfPi * 2], - [-35, 35] - ) - return { - top: verticalAxis, - left: horizontalAxis, - }; - - }) - return ( - - - - - ); -}; diff --git a/src/FLAD/components/Artist.tsx b/src/FLAD/components/Artist.tsx deleted file mode 100644 index 8c7a357..0000000 --- a/src/FLAD/components/Artist.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { View, StyleSheet, Dimensions, Image, TouchableOpacity } from "react-native"; -import Animated, { - Layout, - ZoomIn, - ZoomOut, -} from "react-native-reanimated"; -import { Feather as Icon } from "@expo/vector-icons"; -import Music from "../models/Music"; -import { useState } from "react"; - -const { width } = Dimensions.get("window"); -const SIZE = width / 3; - -interface ArtistProps { - artist: Music; - onPress: () => void; -} - -export default function Artist({ artist, onPress }: ArtistProps) { - const source = typeof artist.image === 'string' ? { uri: artist.image } : artist.image; - const [selected, setSeleted] = useState(false); - const onS = () => { - setSeleted(!selected); - onPress(); - }; - return ( - - - - - {selected && ( - - - - ) - - } - - - - - ); -}; -const styles = StyleSheet.create({ - container: { - width: SIZE, - height: SIZE, - padding: 8, - }, - card: { - flex: 1, - padding: 8, - alignItems: "flex-end", - }, - image: { - borderRadius: 8, - ...StyleSheet.absoluteFillObject, - width: undefined, - height: undefined, - }, - cheked: { - backgroundColor: "white", - borderRadius: 100, - alignItems: "center", - } -}); \ No newline at end of file diff --git a/src/FLAD/components/ArtistChip.tsx b/src/FLAD/components/ArtistChip.tsx deleted file mode 100644 index f20f00e..0000000 --- a/src/FLAD/components/ArtistChip.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { useCallback } from 'react'; -import { View, Text, Image, Pressable, Linking, Alert } from 'react-native' -import Artist from '../models/Artist'; - -interface ArtistChipProps { - backgroundColor: string; - artist: Artist; -} - -export default function ArtistChip({ artist }: ArtistChipProps) { - const handlePress = useCallback(async () => { - const supported = await Linking.canOpenURL(artist.url); - - if (supported) { - await Linking.openURL(artist.url); - } else { - Alert.alert(`Don't know how to open this URL: ${artist.url}`); - } - }, [artist.url]); - - return ( - - - - - - - - ii - - - - ); -}; diff --git a/src/FLAD/components/CardMusicComponent.tsx b/src/FLAD/components/CardMusicComponent.tsx index 14a6d23..9a7b7bc 100644 --- a/src/FLAD/components/CardMusicComponent.tsx +++ b/src/FLAD/components/CardMusicComponent.tsx @@ -26,8 +26,8 @@ export default function CardMusic(props: CardMusicProps) { marginBottom: 15 }, imageContainer: { - width: normalize(92), - height: normalize(92), + width: normalize(82), + height: normalize(82), alignItems: 'center', justifyContent: 'center', marginRight: 20, diff --git a/src/FLAD/components/CircularProgressBar.tsx b/src/FLAD/components/CircularProgressBar.tsx deleted file mode 100644 index 983ea92..0000000 --- a/src/FLAD/components/CircularProgressBar.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { View, StyleSheet } from 'react-native' -import Animated, { lessThan, multiply } from 'react-native-reanimated'; -import HalfCirlce from './HalfCircle'; - -interface CircularProps { - background: string, - foreground: string, - progress: Animated.Value, - radius: number; -} - -const PI = Math.PI; -export default function FladInput({ background, foreground, progress }: CircularProps) { - const theta = multiply(progress, 2 * PI); - const opacity = lessThan(theta, PI); - - return ( - <> - - - - - - - - - - - - - - - ); -}; \ No newline at end of file diff --git a/src/FLAD/components/FriendComponent.tsx b/src/FLAD/components/FriendComponent.tsx index 78015fd..3bc9dc2 100644 --- a/src/FLAD/components/FriendComponent.tsx +++ b/src/FLAD/components/FriendComponent.tsx @@ -61,8 +61,8 @@ export default function Friend(props: FriendProps) { justifyContent: 'center', }, button: { - width: normalize(13), - height: normalize(13), + width: normalize(9), + height: normalize(15), marginRight: 42 } }) diff --git a/src/FLAD/components/HalfCircle.tsx b/src/FLAD/components/HalfCircle.tsx deleted file mode 100644 index 5469a32..0000000 --- a/src/FLAD/components/HalfCircle.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { View } from 'react-native' - -interface HalfCirlceProps { - backgroundColor: string; -} - -export default function HalfCirlce({ backgroundColor }: HalfCirlceProps) { - return ( - - - - - - ); -}; diff --git a/src/FLAD/components/HorizontalFlatListComponent.tsx b/src/FLAD/components/HorizontalFlatList.tsx similarity index 87% rename from src/FLAD/components/HorizontalFlatListComponent.tsx rename to src/FLAD/components/HorizontalFlatList.tsx index 75aaeeb..5df0a3d 100644 --- a/src/FLAD/components/HorizontalFlatListComponent.tsx +++ b/src/FLAD/components/HorizontalFlatList.tsx @@ -7,8 +7,8 @@ interface HorizontalFlatListProps { title: string; data: any[]; } +export const HorizontalFlatList = ({ title, data, children: RenderCell }: HorizontalFlatListProps) => { -export default function HorizontalFlatList({ title, data, children: RenderCell }: HorizontalFlatListProps) { return ( {title} diff --git a/src/FLAD/components/LoadingComponent.tsx b/src/FLAD/components/LoadingComponent.tsx index 36be14e..a628b90 100644 --- a/src/FLAD/components/LoadingComponent.tsx +++ b/src/FLAD/components/LoadingComponent.tsx @@ -26,7 +26,7 @@ export default function Loading() { borderWidth: size / 10, borderColor: "#F80404", shadowColor: "#F40C1C", - shadowOffset: { width: 0, height: 0 }, + //shadowOffset: { width: 0, height: 0 }, shadowOpacity: 1, shadowRadius: 10, }; diff --git a/src/FLAD/components/PaginatorComponent.tsx b/src/FLAD/components/PaginatorComponent.tsx index 62e66e3..0f3c830 100644 --- a/src/FLAD/components/PaginatorComponent.tsx +++ b/src/FLAD/components/PaginatorComponent.tsx @@ -8,7 +8,7 @@ export default function Paginator({ data, scrollX }) { return ( - {data.map((_, i) => { + {data.map((_ : any, i : any) => { const inputRange = [(i - 1) * width, i * width, (i + 1) * width]; const dotWidth = scrollX.interpolate({ diff --git a/src/FLAD/components/littleCard.tsx b/src/FLAD/components/littleCard.tsx new file mode 100644 index 0000000..42ec5e9 --- /dev/null +++ b/src/FLAD/components/littleCard.tsx @@ -0,0 +1,31 @@ +import { View, Text, StyleSheet, Image } from 'react-native'; + +export interface RenderCellProps { + data : any; +} +export const LittleCard = (props: RenderCellProps) => { + return ( + + + {props.data.name} + + + ) +} + +const styles = StyleSheet.create({ + + similarContainer: { + marginHorizontal: 7 + }, + similarTitleFilm: { + color: "#DADADA", + paddingTop: 5, + fontWeight: "300" + }, + similarPoster: { + height: 160, + width: 160, + borderRadius: 16 + } +}) diff --git a/src/FLAD/data/data.ts b/src/FLAD/data/data.ts deleted file mode 100644 index 867a55b..0000000 --- a/src/FLAD/data/data.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Music from "../models/Music"; -import { Spot } from "../models/Spot"; - -export const spotsData: Spot[] = [ - new Spot("1", new Music("6KNw3UKRp3QRsO7Cf4ASVE", - "MOLLY - A COLORS SHOW", - "Tame Impala", - "https://i.scdn.co/image/ab67616d0000b2734299eb40408fc73ce8bf490a", - "https://p.scdn.co/mp3-preview/4faf99856f15e03a09d50b91006efd3205606866?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("2", new Music("5yHoANSze7sGzhn9MUarH3", - "Passat", - "Silk Sonic, Bruno Mars, Anderson .Paak", - "https://i.scdn.co/image/ab67616d0000b273e9df9b5a7df491536c51c922", - "https://p.scdn.co/mp3-preview/0bb7472026a00790950fc231fe61963ef7cc867b?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("3", new Music("7suNqxRED5CrwyZSzYC0nT", - "Extendo", - "Kali Uchis", - "https://i.scdn.co/image/ab67616d0000b273b856464c40a062d1723a21f2", - "https://p.scdn.co/mp3-preview/5398121f6295965e3c7cad8a6dca5667ba7f4713?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("4", new Music("07JqNLmPUJSlcouGQoJlzq", - "Addiction", - "Harry Styles", - "https://i.scdn.co/image/ab67616d0000b2739297f238f237431d56c67460", - "https://p.scdn.co/mp3-preview/33d12e9e5a3dd3394b1649d515912260b01579dd?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("5", new Music("5Ylp75kdffyJSwISRPqEiL", - "La Vidéo", - "Harry Styles", - "https://i.scdn.co/image/ab67616d0000b2738900d48677696015bf325b8b", - "https://p.scdn.co/mp3-preview/4fff3f8d76a422f42cea39f001836a3d54937fc4?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("6", new Music("30d0q6kt1BIfwAQUCAfxVQ", - "Calme toi", - "Kerchack", - "https://i.scdn.co/image/ab67616d0000b273b4f73fb5c5ea299c7ebfbf60", - "https://p.scdn.co/mp3-preview/5de1103b9528c1e47e03d32b0aa5dbfe797191a2?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("7", new Music("7IXQrRgmHxWYWitSlyFY7z", - "Peur (feat. Ziak)", - "Ziak", - "https://i.scdn.co/image/ab67616d0000b273b533a3a5625bc6ce1be56a2e", - "https://p.scdn.co/mp3-preview/2c613f31b11375980aba80a5b535bf87ddb6211b?cid=774b29d4f13844c495f206cafdad9c86") - ), - new Spot("8", new Music("7wBoSW48q4ZFe8qSdozqqi", - "Blue", - "Kerchack", - "https://i.scdn.co/image/ab67616d0000b273cc4e66af40292c9d92146909", - "https://p.scdn.co/mp3-preview/401b51374dd3f2a15466a0b415a9ac7d2114a54b?cid=774b29d4f13844c495f206cafdad9c86") - ) - -]; diff --git a/src/FLAD/lib/index.js b/src/FLAD/lib/index.js deleted file mode 100644 index 14f24b2..0000000 --- a/src/FLAD/lib/index.js +++ /dev/null @@ -1,7 +0,0 @@ -Object.defineProperty(exports, '__esModule', {value: true}); -require('./mqttLib'); -const storage = require('./storage'); -function initialize() { - global.localStorage = storage; -} -exports.default = initialize; \ No newline at end of file diff --git a/src/FLAD/lib/mqtt.js b/src/FLAD/lib/mqtt.js deleted file mode 100644 index 612845e..0000000 --- a/src/FLAD/lib/mqtt.js +++ /dev/null @@ -1,2395 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2013 IBM Corp. - * - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the Eclipse Public License v1.0 - * and Eclipse Distribution License v1.0 which accompany this distribution. - * - * The Eclipse Public License is available at - * http://www.eclipse.org/legal/epl-v10.html - * and the Eclipse Distribution License is available at - * http://www.eclipse.org/org/documents/edl-v10.php. - * - * Contributors: - * Andrew Banks - initial API and implementation and initial documentation - *******************************************************************************/ - - -// Only expose a single object name in the global namespace. -// Everything must go through this module. Global Paho module -// only has a single public function, client, which returns -// a Paho client object given connection details. - -/** - * Send and receive messages using web browsers. - *

- * This programming interface lets a JavaScript client application use the MQTT V3.1 or - * V3.1.1 protocol to connect to an MQTT-supporting messaging server. - * - * The function supported includes: - *

    - *
  1. Connecting to and disconnecting from a server. The server is identified by its host name and port number. - *
  2. Specifying options that relate to the communications link with the server, - * for example the frequency of keep-alive heartbeats, and whether SSL/TLS is required. - *
  3. Subscribing to and receiving messages from MQTT Topics. - *
  4. Publishing messages to MQTT Topics. - *
- *

- * The API consists of two main objects: - *

- *
{@link Paho.Client}
- *
This contains methods that provide the functionality of the API, - * including provision of callbacks that notify the application when a message - * arrives from or is delivered to the messaging server, - * or when the status of its connection to the messaging server changes.
- *
{@link Paho.Message}
- *
This encapsulates the payload of the message along with various attributes - * associated with its delivery, in particular the destination to which it has - * been (or is about to be) sent.
- *
- *

- * The programming interface validates parameters passed to it, and will throw - * an Error containing an error message intended for developer use, if it detects - * an error with any parameter. - *

- * Example: - * - *

-var client = new Paho.MQTT.Client(location.hostname, Number(location.port), "clientId");
-client.onConnectionLost = onConnectionLost;
-client.onMessageArrived = onMessageArrived;
-client.connect({onSuccess:onConnect});
-
-function onConnect() {
-  // Once a connection has been made, make a subscription and send a message.
-  console.log("onConnect");
-  client.subscribe("/World");
-  var message = new Paho.MQTT.Message("Hello");
-  message.destinationName = "/World";
-  client.send(message);
-};
-function onConnectionLost(responseObject) {
-  if (responseObject.errorCode !== 0)
-	console.log("onConnectionLost:"+responseObject.errorMessage);
-};
-function onMessageArrived(message) {
-  console.log("onMessageArrived:"+message.payloadString);
-  client.disconnect();
-};
- * 
- * @namespace Paho - */ - -/* jshint shadow:true */ -(function ExportLibrary(root, factory) { - if(typeof exports === "object" && typeof module === "object"){ - module.exports = factory(); - } else if (typeof define === "function" && define.amd){ - define(factory); - } else if (typeof exports === "object"){ - exports = factory(); - } else { - //if (typeof root.Paho === "undefined"){ - // root.Paho = {}; - //} - root.Paho = factory(); - } -})(this, function LibraryFactory(){ - - - var PahoMQTT = (function (global) { - - // Private variables below, these are only visible inside the function closure - // which is used to define the module. - var version = "@VERSION@-@BUILDLEVEL@"; - - /** - * @private - */ - var localStorage = global.localStorage || (function () { - var data = {}; - - return { - setItem: function (key, item) { data[key] = item; }, - getItem: function (key) { return data[key]; }, - removeItem: function (key) { delete data[key]; }, - }; - })(); - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var MESSAGE_TYPE = { - CONNECT: 1, - CONNACK: 2, - PUBLISH: 3, - PUBACK: 4, - PUBREC: 5, - PUBREL: 6, - PUBCOMP: 7, - SUBSCRIBE: 8, - SUBACK: 9, - UNSUBSCRIBE: 10, - UNSUBACK: 11, - PINGREQ: 12, - PINGRESP: 13, - DISCONNECT: 14 - }; - - // Collection of utility methods used to simplify module code - // and promote the DRY pattern. - - /** - * Validate an object's parameter names to ensure they - * match a list of expected variables name for this option - * type. Used to ensure option object passed into the API don't - * contain erroneous parameters. - * @param {Object} obj - User options object - * @param {Object} keys - valid keys and types that may exist in obj. - * @throws {Error} Invalid option parameter found. - * @private - */ - var validate = function(obj, keys) { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - if (keys.hasOwnProperty(key)) { - if (typeof obj[key] !== keys[key]) - throw new Error(format(ERROR.INVALID_TYPE, [typeof obj[key], key])); - } else { - var errorStr = "Unknown property, " + key + ". Valid properties are:"; - for (var validKey in keys) - if (keys.hasOwnProperty(validKey)) - errorStr = errorStr+" "+validKey; - throw new Error(errorStr); - } - } - } - }; - - /** - * Return a new function which runs the user function bound - * to a fixed scope. - * @param {function} User function - * @param {object} Function scope - * @return {function} User function bound to another scope - * @private - */ - var scope = function (f, scope) { - return function () { - return f.apply(scope, arguments); - }; - }; - - /** - * Unique message type identifiers, with associated - * associated integer values. - * @private - */ - var ERROR = { - OK: {code:0, text:"AMQJSC0000I OK."}, - CONNECT_TIMEOUT: {code:1, text:"AMQJSC0001E Connect timed out."}, - SUBSCRIBE_TIMEOUT: {code:2, text:"AMQJS0002E Subscribe timed out."}, - UNSUBSCRIBE_TIMEOUT: {code:3, text:"AMQJS0003E Unsubscribe timed out."}, - PING_TIMEOUT: {code:4, text:"AMQJS0004E Ping timed out."}, - INTERNAL_ERROR: {code:5, text:"AMQJS0005E Internal error. Error Message: {0}, Stack trace: {1}"}, - CONNACK_RETURNCODE: {code:6, text:"AMQJS0006E Bad Connack return code:{0} {1}."}, - SOCKET_ERROR: {code:7, text:"AMQJS0007E Socket error:{0}."}, - SOCKET_CLOSE: {code:8, text:"AMQJS0008I Socket closed."}, - MALFORMED_UTF: {code:9, text:"AMQJS0009E Malformed UTF data:{0} {1} {2}."}, - UNSUPPORTED: {code:10, text:"AMQJS0010E {0} is not supported by this browser."}, - INVALID_STATE: {code:11, text:"AMQJS0011E Invalid state {0}."}, - INVALID_TYPE: {code:12, text:"AMQJS0012E Invalid type {0} for {1}."}, - INVALID_ARGUMENT: {code:13, text:"AMQJS0013E Invalid argument {0} for {1}."}, - UNSUPPORTED_OPERATION: {code:14, text:"AMQJS0014E Unsupported operation."}, - INVALID_STORED_DATA: {code:15, text:"AMQJS0015E Invalid data in local storage key={0} value={1}."}, - INVALID_MQTT_MESSAGE_TYPE: {code:16, text:"AMQJS0016E Invalid MQTT message type {0}."}, - MALFORMED_UNICODE: {code:17, text:"AMQJS0017E Malformed Unicode string:{0} {1}."}, - BUFFER_FULL: {code:18, text:"AMQJS0018E Message buffer is full, maximum buffer size: {0}."}, - }; - - /** CONNACK RC Meaning. */ - var CONNACK_RC = { - 0:"Connection Accepted", - 1:"Connection Refused: unacceptable protocol version", - 2:"Connection Refused: identifier rejected", - 3:"Connection Refused: server unavailable", - 4:"Connection Refused: bad user name or password", - 5:"Connection Refused: not authorized" - }; - - /** - * Format an error message text. - * @private - * @param {error} ERROR value above. - * @param {substitutions} [array] substituted into the text. - * @return the text with the substitutions made. - */ - var format = function(error, substitutions) { - var text = error.text; - if (substitutions) { - var field,start; - for (var i=0; i 0) { - var part1 = text.substring(0,start); - var part2 = text.substring(start+field.length); - text = part1+substitutions[i]+part2; - } - } - } - return text; - }; - - //MQTT protocol and version 6 M Q I s d p 3 - var MqttProtoIdentifierv3 = [0x00,0x06,0x4d,0x51,0x49,0x73,0x64,0x70,0x03]; - //MQTT proto/version for 311 4 M Q T T 4 - var MqttProtoIdentifierv4 = [0x00,0x04,0x4d,0x51,0x54,0x54,0x04]; - - /** - * Construct an MQTT wire protocol message. - * @param type MQTT packet type. - * @param options optional wire message attributes. - * - * Optional properties - * - * messageIdentifier: message ID in the range [0..65535] - * payloadMessage: Application Message - PUBLISH only - * connectStrings: array of 0 or more Strings to be put into the CONNECT payload - * topics: array of strings (SUBSCRIBE, UNSUBSCRIBE) - * requestQoS: array of QoS values [0..2] - * - * "Flag" properties - * cleanSession: true if present / false if absent (CONNECT) - * willMessage: true if present / false if absent (CONNECT) - * isRetained: true if present / false if absent (CONNECT) - * userName: true if present / false if absent (CONNECT) - * password: true if present / false if absent (CONNECT) - * keepAliveInterval: integer [0..65535] (CONNECT) - * - * @private - * @ignore - */ - var WireMessage = function (type, options) { - this.type = type; - for (var name in options) { - if (options.hasOwnProperty(name)) { - this[name] = options[name]; - } - } - }; - - WireMessage.prototype.encode = function() { - // Compute the first byte of the fixed header - var first = ((this.type & 0x0f) << 4); - - /* - * Now calculate the length of the variable header + payload by adding up the lengths - * of all the component parts - */ - - var remLength = 0; - var topicStrLength = []; - var destinationNameLength = 0; - var willMessagePayloadBytes; - - // if the message contains a messageIdentifier then we need two bytes for that - if (this.messageIdentifier !== undefined) - remLength += 2; - - switch(this.type) { - // If this a Connect then we need to include 12 bytes for its header - case MESSAGE_TYPE.CONNECT: - switch(this.mqttVersion) { - case 3: - remLength += MqttProtoIdentifierv3.length + 3; - break; - case 4: - remLength += MqttProtoIdentifierv4.length + 3; - break; - } - - remLength += UTF8Length(this.clientId) + 2; - if (this.willMessage !== undefined) { - remLength += UTF8Length(this.willMessage.destinationName) + 2; - // Will message is always a string, sent as UTF-8 characters with a preceding length. - willMessagePayloadBytes = this.willMessage.payloadBytes; - if (!(willMessagePayloadBytes instanceof Uint8Array)) - willMessagePayloadBytes = new Uint8Array(payloadBytes); - remLength += willMessagePayloadBytes.byteLength +2; - } - if (this.userName !== undefined) - remLength += UTF8Length(this.userName) + 2; - if (this.password !== undefined) - remLength += UTF8Length(this.password) + 2; - break; - - // Subscribe, Unsubscribe can both contain topic strings - case MESSAGE_TYPE.SUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - remLength += this.requestedQos.length; // 1 byte for each topic's Qos - // QoS on Subscribe only - break; - - case MESSAGE_TYPE.UNSUBSCRIBE: - first |= 0x02; // Qos = 1; - for ( var i = 0; i < this.topics.length; i++) { - topicStrLength[i] = UTF8Length(this.topics[i]); - remLength += topicStrLength[i] + 2; - } - break; - - case MESSAGE_TYPE.PUBREL: - first |= 0x02; // Qos = 1; - break; - - case MESSAGE_TYPE.PUBLISH: - if (this.payloadMessage.duplicate) first |= 0x08; - first = first |= (this.payloadMessage.qos << 1); - if (this.payloadMessage.retained) first |= 0x01; - destinationNameLength = UTF8Length(this.payloadMessage.destinationName); - remLength += destinationNameLength + 2; - var payloadBytes = this.payloadMessage.payloadBytes; - remLength += payloadBytes.byteLength; - if (payloadBytes instanceof ArrayBuffer) - payloadBytes = new Uint8Array(payloadBytes); - else if (!(payloadBytes instanceof Uint8Array)) - payloadBytes = new Uint8Array(payloadBytes.buffer); - break; - - case MESSAGE_TYPE.DISCONNECT: - break; - - default: - break; - } - - // Now we can allocate a buffer for the message - - var mbi = encodeMBI(remLength); // Convert the length to MQTT MBI format - var pos = mbi.length + 1; // Offset of start of variable header - var buffer = new ArrayBuffer(remLength + pos); - var byteStream = new Uint8Array(buffer); // view it as a sequence of bytes - - //Write the fixed header into the buffer - byteStream[0] = first; - byteStream.set(mbi,1); - - // If this is a PUBLISH then the variable header starts with a topic - if (this.type == MESSAGE_TYPE.PUBLISH) - pos = writeString(this.payloadMessage.destinationName, destinationNameLength, byteStream, pos); - // If this is a CONNECT then the variable header contains the protocol name/version, flags and keepalive time - - else if (this.type == MESSAGE_TYPE.CONNECT) { - switch (this.mqttVersion) { - case 3: - byteStream.set(MqttProtoIdentifierv3, pos); - pos += MqttProtoIdentifierv3.length; - break; - case 4: - byteStream.set(MqttProtoIdentifierv4, pos); - pos += MqttProtoIdentifierv4.length; - break; - } - var connectFlags = 0; - if (this.cleanSession) - connectFlags = 0x02; - if (this.willMessage !== undefined ) { - connectFlags |= 0x04; - connectFlags |= (this.willMessage.qos<<3); - if (this.willMessage.retained) { - connectFlags |= 0x20; - } - } - if (this.userName !== undefined) - connectFlags |= 0x80; - if (this.password !== undefined) - connectFlags |= 0x40; - byteStream[pos++] = connectFlags; - pos = writeUint16 (this.keepAliveInterval, byteStream, pos); - } - - // Output the messageIdentifier - if there is one - if (this.messageIdentifier !== undefined) - pos = writeUint16 (this.messageIdentifier, byteStream, pos); - - switch(this.type) { - case MESSAGE_TYPE.CONNECT: - pos = writeString(this.clientId, UTF8Length(this.clientId), byteStream, pos); - if (this.willMessage !== undefined) { - pos = writeString(this.willMessage.destinationName, UTF8Length(this.willMessage.destinationName), byteStream, pos); - pos = writeUint16(willMessagePayloadBytes.byteLength, byteStream, pos); - byteStream.set(willMessagePayloadBytes, pos); - pos += willMessagePayloadBytes.byteLength; - - } - if (this.userName !== undefined) - pos = writeString(this.userName, UTF8Length(this.userName), byteStream, pos); - if (this.password !== undefined) - pos = writeString(this.password, UTF8Length(this.password), byteStream, pos); - break; - - case MESSAGE_TYPE.PUBLISH: - // PUBLISH has a text or binary payload, if text do not add a 2 byte length field, just the UTF characters. - byteStream.set(payloadBytes, pos); - - break; - - // case MESSAGE_TYPE.PUBREC: - // case MESSAGE_TYPE.PUBREL: - // case MESSAGE_TYPE.PUBCOMP: - // break; - - case MESSAGE_TYPE.SUBSCRIBE: - // SUBSCRIBE has a list of topic strings and request QoS - for (var i=0; i> 4; - var messageInfo = first &= 0x0f; - pos += 1; - - - // Decode the remaining length (MBI format) - - var digit; - var remLength = 0; - var multiplier = 1; - do { - if (pos == input.length) { - return [null,startingPos]; - } - digit = input[pos++]; - remLength += ((digit & 0x7F) * multiplier); - multiplier *= 128; - } while ((digit & 0x80) !== 0); - - var endPos = pos+remLength; - if (endPos > input.length) { - return [null,startingPos]; - } - - var wireMessage = new WireMessage(type); - switch(type) { - case MESSAGE_TYPE.CONNACK: - var connectAcknowledgeFlags = input[pos++]; - if (connectAcknowledgeFlags & 0x01) - wireMessage.sessionPresent = true; - wireMessage.returnCode = input[pos++]; - break; - - case MESSAGE_TYPE.PUBLISH: - var qos = (messageInfo >> 1) & 0x03; - - var len = readUint16(input, pos); - pos += 2; - var topicName = parseUTF8(input, pos, len); - pos += len; - // If QoS 1 or 2 there will be a messageIdentifier - if (qos > 0) { - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - } - - var message = new Message(input.subarray(pos, endPos)); - if ((messageInfo & 0x01) == 0x01) - message.retained = true; - if ((messageInfo & 0x08) == 0x08) - message.duplicate = true; - message.qos = qos; - message.destinationName = topicName; - wireMessage.payloadMessage = message; - break; - - case MESSAGE_TYPE.PUBACK: - case MESSAGE_TYPE.PUBREC: - case MESSAGE_TYPE.PUBREL: - case MESSAGE_TYPE.PUBCOMP: - case MESSAGE_TYPE.UNSUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - break; - - case MESSAGE_TYPE.SUBACK: - wireMessage.messageIdentifier = readUint16(input, pos); - pos += 2; - wireMessage.returnCode = input.subarray(pos, endPos); - break; - - default: - break; - } - - return [wireMessage,endPos]; - } - - function writeUint16(input, buffer, offset) { - buffer[offset++] = input >> 8; //MSB - buffer[offset++] = input % 256; //LSB - return offset; - } - - function writeString(input, utf8Length, buffer, offset) { - offset = writeUint16(utf8Length, buffer, offset); - stringToUTF8(input, buffer, offset); - return offset + utf8Length; - } - - function readUint16(buffer, offset) { - return 256*buffer[offset] + buffer[offset+1]; - } - - /** - * Encodes an MQTT Multi-Byte Integer - * @private - */ - function encodeMBI(number) { - var output = new Array(1); - var numBytes = 0; - - do { - var digit = number % 128; - number = number >> 7; - if (number > 0) { - digit |= 0x80; - } - output[numBytes++] = digit; - } while ( (number > 0) && (numBytes<4) ); - - return output; - } - - /** - * Takes a String and calculates its length in bytes when encoded in UTF8. - * @private - */ - function UTF8Length(input) { - var output = 0; - for (var i = 0; i 0x7FF) - { - // Surrogate pair means its a 4 byte character - if (0xD800 <= charCode && charCode <= 0xDBFF) - { - i++; - output++; - } - output +=3; - } - else if (charCode > 0x7F) - output +=2; - else - output++; - } - return output; - } - - /** - * Takes a String and writes it into an array as UTF8 encoded bytes. - * @private - */ - function stringToUTF8(input, output, start) { - var pos = start; - for (var i = 0; i>6 & 0x1F | 0xC0; - output[pos++] = charCode & 0x3F | 0x80; - } else if (charCode <= 0xFFFF) { - output[pos++] = charCode>>12 & 0x0F | 0xE0; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - } else { - output[pos++] = charCode>>18 & 0x07 | 0xF0; - output[pos++] = charCode>>12 & 0x3F | 0x80; - output[pos++] = charCode>>6 & 0x3F | 0x80; - output[pos++] = charCode & 0x3F | 0x80; - } - } - return output; - } - - function parseUTF8(input, offset, length) { - var output = ""; - var utf16; - var pos = offset; - - while (pos < offset+length) - { - var byte1 = input[pos++]; - if (byte1 < 128) - utf16 = byte1; - else - { - var byte2 = input[pos++]-128; - if (byte2 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16),""])); - if (byte1 < 0xE0) // 2 byte character - utf16 = 64*(byte1-0xC0) + byte2; - else - { - var byte3 = input[pos++]-128; - if (byte3 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16)])); - if (byte1 < 0xF0) // 3 byte character - utf16 = 4096*(byte1-0xE0) + 64*byte2 + byte3; - else - { - var byte4 = input[pos++]-128; - if (byte4 < 0) - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - if (byte1 < 0xF8) // 4 byte character - utf16 = 262144*(byte1-0xF0) + 4096*byte2 + 64*byte3 + byte4; - else // longer encodings are not supported - throw new Error(format(ERROR.MALFORMED_UTF, [byte1.toString(16), byte2.toString(16), byte3.toString(16), byte4.toString(16)])); - } - } - } - - if (utf16 > 0xFFFF) // 4 byte character - express as a surrogate pair - { - utf16 -= 0x10000; - output += String.fromCharCode(0xD800 + (utf16 >> 10)); // lead character - utf16 = 0xDC00 + (utf16 & 0x3FF); // trail character - } - output += String.fromCharCode(utf16); - } - return output; - } - - /** - * Repeat keepalive requests, monitor responses. - * @ignore - */ - var Pinger = function(client, keepAliveInterval) { - this._client = client; - this._keepAliveInterval = keepAliveInterval*1000; - this.isReset = false; - - var pingReq = new WireMessage(MESSAGE_TYPE.PINGREQ).encode(); - - var doTimeout = function (pinger) { - return function () { - return doPing.apply(pinger); - }; - }; - - /** @ignore */ - var doPing = function() { - if (!this.isReset) { - this._client._trace("Pinger.doPing", "Timed out"); - this._client._disconnected( ERROR.PING_TIMEOUT.code , format(ERROR.PING_TIMEOUT)); - } else { - this.isReset = false; - this._client._trace("Pinger.doPing", "send PINGREQ"); - this._client.socket.send(pingReq); - this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); - } - }; - - this.reset = function() { - this.isReset = true; - clearTimeout(this.timeout); - if (this._keepAliveInterval > 0) - this.timeout = setTimeout(doTimeout(this), this._keepAliveInterval); - }; - - this.cancel = function() { - clearTimeout(this.timeout); - }; - }; - - /** - * Monitor request completion. - * @ignore - */ - var Timeout = function(client, timeoutSeconds, action, args) { - if (!timeoutSeconds) - timeoutSeconds = 30; - - var doTimeout = function (action, client, args) { - return function () { - return action.apply(client, args); - }; - }; - this.timeout = setTimeout(doTimeout(action, client, args), timeoutSeconds * 1000); - - this.cancel = function() { - clearTimeout(this.timeout); - }; - }; - - /** - * Internal implementation of the Websockets MQTT V3.1 client. - * - * @name Paho.ClientImpl @constructor - * @param {String} host the DNS nameof the webSocket host. - * @param {Number} port the port number for that host. - * @param {String} clientId the MQ client identifier. - */ - var ClientImpl = function (uri, host, port, path, clientId) { - // Check dependencies are satisfied in this browser. - if (!("WebSocket" in global && global.WebSocket !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["WebSocket"])); - } - if (!("ArrayBuffer" in global && global.ArrayBuffer !== null)) { - throw new Error(format(ERROR.UNSUPPORTED, ["ArrayBuffer"])); - } - this._trace("Paho.Client", uri, host, port, path, clientId); - - this.host = host; - this.port = port; - this.path = path; - this.uri = uri; - this.clientId = clientId; - this._wsuri = null; - - // Local storagekeys are qualified with the following string. - // The conditional inclusion of path in the key is for backward - // compatibility to when the path was not configurable and assumed to - // be /mqtt - this._localKey=host+":"+port+(path!="/mqtt"?":"+path:"")+":"+clientId+":"; - - // Create private instance-only message queue - // Internal queue of messages to be sent, in sending order. - this._msg_queue = []; - this._buffered_msg_queue = []; - - // Messages we have sent and are expecting a response for, indexed by their respective message ids. - this._sentMessages = {}; - - // Messages we have received and acknowleged and are expecting a confirm message for - // indexed by their respective message ids. - this._receivedMessages = {}; - - // Internal list of callbacks to be executed when messages - // have been successfully sent over web socket, e.g. disconnect - // when it doesn't have to wait for ACK, just message is dispatched. - this._notify_msg_sent = {}; - - // Unique identifier for SEND messages, incrementing - // counter as messages are sent. - this._message_identifier = 1; - - // Used to determine the transmission sequence of stored sent messages. - this._sequence = 0; - - - // Load the local state, if any, from the saved version, only restore state relevant to this client. - for (var key in localStorage) - if ( key.indexOf("Sent:"+this._localKey) === 0 || key.indexOf("Received:"+this._localKey) === 0) - this.restore(key); - }; - - // Messaging Client public instance members. - ClientImpl.prototype.host = null; - ClientImpl.prototype.port = null; - ClientImpl.prototype.path = null; - ClientImpl.prototype.uri = null; - ClientImpl.prototype.clientId = null; - - // Messaging Client private instance members. - ClientImpl.prototype.socket = null; - /* true once we have received an acknowledgement to a CONNECT packet. */ - ClientImpl.prototype.connected = false; - /* The largest message identifier allowed, may not be larger than 2**16 but - * if set smaller reduces the maximum number of outbound messages allowed. - */ - ClientImpl.prototype.maxMessageIdentifier = 65536; - ClientImpl.prototype.connectOptions = null; - ClientImpl.prototype.hostIndex = null; - ClientImpl.prototype.onConnected = null; - ClientImpl.prototype.onConnectionLost = null; - ClientImpl.prototype.onMessageDelivered = null; - ClientImpl.prototype.onMessageArrived = null; - ClientImpl.prototype.traceFunction = null; - ClientImpl.prototype._msg_queue = null; - ClientImpl.prototype._buffered_msg_queue = null; - ClientImpl.prototype._connectTimeout = null; - /* The sendPinger monitors how long we allow before we send data to prove to the server that we are alive. */ - ClientImpl.prototype.sendPinger = null; - /* The receivePinger monitors how long we allow before we require evidence that the server is alive. */ - ClientImpl.prototype.receivePinger = null; - ClientImpl.prototype._reconnectInterval = 1; // Reconnect Delay, starts at 1 second - ClientImpl.prototype._reconnecting = false; - ClientImpl.prototype._reconnectTimeout = null; - ClientImpl.prototype.disconnectedPublishing = false; - ClientImpl.prototype.disconnectedBufferSize = 5000; - - ClientImpl.prototype.receiveBuffer = null; - - ClientImpl.prototype._traceBuffer = null; - ClientImpl.prototype._MAX_TRACE_ENTRIES = 100; - - ClientImpl.prototype.connect = function (connectOptions) { - var connectOptionsMasked = this._traceMask(connectOptions, "password"); - this._trace("Client.connect", connectOptionsMasked, this.socket, this.connected); - - if (this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - if (this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["already connected"])); - - if (this._reconnecting) { - // connect() function is called while reconnect is in progress. - // Terminate the auto reconnect process to use new connect options. - this._reconnectTimeout.cancel(); - this._reconnectTimeout = null; - this._reconnecting = false; - } - - this.connectOptions = connectOptions; - this._reconnectInterval = 1; - this._reconnecting = false; - if (connectOptions.uris) { - this.hostIndex = 0; - this._doConnect(connectOptions.uris[0]); - } else { - this._doConnect(this.uri); - } - - }; - - ClientImpl.prototype.subscribe = function (filter, subscribeOptions) { - this._trace("Client.subscribe", filter, subscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.SUBSCRIBE); - wireMessage.topics = filter.constructor === Array ? filter : [filter]; - if (subscribeOptions.qos === undefined) - subscribeOptions.qos = 0; - wireMessage.requestedQos = []; - for (var i = 0; i < wireMessage.topics.length; i++) - wireMessage.requestedQos[i] = subscribeOptions.qos; - - if (subscribeOptions.onSuccess) { - wireMessage.onSuccess = function(grantedQos) {subscribeOptions.onSuccess({invocationContext:subscribeOptions.invocationContext,grantedQos:grantedQos});}; - } - - if (subscribeOptions.onFailure) { - wireMessage.onFailure = function(errorCode) {subscribeOptions.onFailure({invocationContext:subscribeOptions.invocationContext,errorCode:errorCode, errorMessage:format(errorCode)});}; - } - - if (subscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, subscribeOptions.timeout, subscribeOptions.onFailure, - [{invocationContext:subscribeOptions.invocationContext, - errorCode:ERROR.SUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.SUBSCRIBE_TIMEOUT)}]); - } - - // All subscriptions return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - /** @ignore */ - ClientImpl.prototype.unsubscribe = function(filter, unsubscribeOptions) { - this._trace("Client.unsubscribe", filter, unsubscribeOptions); - - if (!this.connected) - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.UNSUBSCRIBE); - wireMessage.topics = filter.constructor === Array ? filter : [filter]; - - if (unsubscribeOptions.onSuccess) { - wireMessage.callback = function() {unsubscribeOptions.onSuccess({invocationContext:unsubscribeOptions.invocationContext});}; - } - if (unsubscribeOptions.timeout) { - wireMessage.timeOut = new Timeout(this, unsubscribeOptions.timeout, unsubscribeOptions.onFailure, - [{invocationContext:unsubscribeOptions.invocationContext, - errorCode:ERROR.UNSUBSCRIBE_TIMEOUT.code, - errorMessage:format(ERROR.UNSUBSCRIBE_TIMEOUT)}]); - } - - // All unsubscribes return a SUBACK. - this._requires_ack(wireMessage); - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.send = function (message) { - this._trace("Client.send", message); - - var wireMessage = new WireMessage(MESSAGE_TYPE.PUBLISH); - wireMessage.payloadMessage = message; - - if (this.connected) { - // Mark qos 1 & 2 message as "ACK required" - // For qos 0 message, invoke onMessageDelivered callback if there is one. - // Then schedule the message. - if (message.qos > 0) { - this._requires_ack(wireMessage); - } else if (this.onMessageDelivered) { - this._notify_msg_sent[wireMessage] = this.onMessageDelivered(wireMessage.payloadMessage); - } - this._schedule_message(wireMessage); - } else { - // Currently disconnected, will not schedule this message - // Check if reconnecting is in progress and disconnected publish is enabled. - if (this._reconnecting && this.disconnectedPublishing) { - // Check the limit which include the "required ACK" messages - var messageCount = Object.keys(this._sentMessages).length + this._buffered_msg_queue.length; - if (messageCount > this.disconnectedBufferSize) { - throw new Error(format(ERROR.BUFFER_FULL, [this.disconnectedBufferSize])); - } else { - if (message.qos > 0) { - // Mark this message as "ACK required" - this._requires_ack(wireMessage); - } else { - wireMessage.sequence = ++this._sequence; - // Add messages in fifo order to array, by adding to start - this._buffered_msg_queue.unshift(wireMessage); - } - } - } else { - throw new Error(format(ERROR.INVALID_STATE, ["not connected"])); - } - } - }; - - ClientImpl.prototype.disconnect = function () { - this._trace("Client.disconnect"); - - if (this._reconnecting) { - // disconnect() function is called while reconnect is in progress. - // Terminate the auto reconnect process. - this._reconnectTimeout.cancel(); - this._reconnectTimeout = null; - this._reconnecting = false; - } - - if (!this.socket) - throw new Error(format(ERROR.INVALID_STATE, ["not connecting or connected"])); - - var wireMessage = new WireMessage(MESSAGE_TYPE.DISCONNECT); - - // Run the disconnected call back as soon as the message has been sent, - // in case of a failure later on in the disconnect processing. - // as a consequence, the _disconected call back may be run several times. - this._notify_msg_sent[wireMessage] = scope(this._disconnected, this); - - this._schedule_message(wireMessage); - }; - - ClientImpl.prototype.getTraceLog = function () { - if ( this._traceBuffer !== null ) { - this._trace("Client.getTraceLog", new Date()); - this._trace("Client.getTraceLog in flight messages", this._sentMessages.length); - for (var key in this._sentMessages) - this._trace("_sentMessages ",key, this._sentMessages[key]); - for (var key in this._receivedMessages) - this._trace("_receivedMessages ",key, this._receivedMessages[key]); - - return this._traceBuffer; - } - }; - - ClientImpl.prototype.startTrace = function () { - if ( this._traceBuffer === null ) { - this._traceBuffer = []; - } - this._trace("Client.startTrace", new Date(), version); - }; - - ClientImpl.prototype.stopTrace = function () { - delete this._traceBuffer; - }; - - ClientImpl.prototype._doConnect = function (wsurl) { - // When the socket is open, this client will send the CONNECT WireMessage using the saved parameters. - if (this.connectOptions.useSSL) { - var uriParts = wsurl.split(":"); - uriParts[0] = "wss"; - wsurl = uriParts.join(":"); - } - this._wsuri = wsurl; - this.connected = false; - - - - if (this.connectOptions.mqttVersion < 4) { - this.socket = new WebSocket(wsurl, ["mqttv3.1"]); - } else { - this.socket = new WebSocket(wsurl, ["mqtt"]); - } - this.socket.binaryType = "arraybuffer"; - this.socket.onopen = scope(this._on_socket_open, this); - this.socket.onmessage = scope(this._on_socket_message, this); - this.socket.onerror = scope(this._on_socket_error, this); - this.socket.onclose = scope(this._on_socket_close, this); - - this.sendPinger = new Pinger(this, this.connectOptions.keepAliveInterval); - this.receivePinger = new Pinger(this, this.connectOptions.keepAliveInterval); - if (this._connectTimeout) { - this._connectTimeout.cancel(); - this._connectTimeout = null; - } - this._connectTimeout = new Timeout(this, this.connectOptions.timeout, this._disconnected, [ERROR.CONNECT_TIMEOUT.code, format(ERROR.CONNECT_TIMEOUT)]); - }; - - - // Schedule a new message to be sent over the WebSockets - // connection. CONNECT messages cause WebSocket connection - // to be started. All other messages are queued internally - // until this has happened. When WS connection starts, process - // all outstanding messages. - ClientImpl.prototype._schedule_message = function (message) { - // Add messages in fifo order to array, by adding to start - this._msg_queue.unshift(message); - // Process outstanding messages in the queue if we have an open socket, and have received CONNACK. - if (this.connected) { - this._process_queue(); - } - }; - - ClientImpl.prototype.store = function(prefix, wireMessage) { - var storedMessage = {type:wireMessage.type, messageIdentifier:wireMessage.messageIdentifier, version:1}; - - switch(wireMessage.type) { - case MESSAGE_TYPE.PUBLISH: - if(wireMessage.pubRecReceived) - storedMessage.pubRecReceived = true; - - // Convert the payload to a hex string. - storedMessage.payloadMessage = {}; - var hex = ""; - var messageBytes = wireMessage.payloadMessage.payloadBytes; - for (var i=0; i= 2) { - var x = parseInt(hex.substring(0, 2), 16); - hex = hex.substring(2, hex.length); - byteStream[i++] = x; - } - var payloadMessage = new Message(byteStream); - - payloadMessage.qos = storedMessage.payloadMessage.qos; - payloadMessage.destinationName = storedMessage.payloadMessage.destinationName; - if (storedMessage.payloadMessage.duplicate) - payloadMessage.duplicate = true; - if (storedMessage.payloadMessage.retained) - payloadMessage.retained = true; - wireMessage.payloadMessage = payloadMessage; - - break; - - default: - throw Error(format(ERROR.INVALID_STORED_DATA, [key, value])); - } - - if (key.indexOf("Sent:"+this._localKey) === 0) { - wireMessage.payloadMessage.duplicate = true; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - } else if (key.indexOf("Received:"+this._localKey) === 0) { - this._receivedMessages[wireMessage.messageIdentifier] = wireMessage; - } - }; - - ClientImpl.prototype._process_queue = function () { - var message = null; - - // Send all queued messages down socket connection - while ((message = this._msg_queue.pop())) { - this._socket_send(message); - // Notify listeners that message was successfully sent - if (this._notify_msg_sent[message]) { - this._notify_msg_sent[message](); - delete this._notify_msg_sent[message]; - } - } - }; - - /** - * Expect an ACK response for this message. Add message to the set of in progress - * messages and set an unused identifier in this message. - * @ignore - */ - ClientImpl.prototype._requires_ack = function (wireMessage) { - var messageCount = Object.keys(this._sentMessages).length; - if (messageCount > this.maxMessageIdentifier) - throw Error ("Too many messages:"+messageCount); - - while(this._sentMessages[this._message_identifier] !== undefined) { - this._message_identifier++; - } - wireMessage.messageIdentifier = this._message_identifier; - this._sentMessages[wireMessage.messageIdentifier] = wireMessage; - if (wireMessage.type === MESSAGE_TYPE.PUBLISH) { - this.store("Sent:", wireMessage); - } - if (this._message_identifier === this.maxMessageIdentifier) { - this._message_identifier = 1; - } - }; - - /** - * Called when the underlying websocket has been opened. - * @ignore - */ - ClientImpl.prototype._on_socket_open = function () { - // Create the CONNECT message object. - var wireMessage = new WireMessage(MESSAGE_TYPE.CONNECT, this.connectOptions); - wireMessage.clientId = this.clientId; - this._socket_send(wireMessage); - }; - - /** - * Called when the underlying websocket has received a complete packet. - * @ignore - */ - ClientImpl.prototype._on_socket_message = function (event) { - this._trace("Client._on_socket_message", event.data); - var messages = this._deframeMessages(event.data); - for (var i = 0; i < messages.length; i+=1) { - this._handleMessage(messages[i]); - } - }; - - ClientImpl.prototype._deframeMessages = function(data) { - var byteArray = new Uint8Array(data); - var messages = []; - if (this.receiveBuffer) { - var newData = new Uint8Array(this.receiveBuffer.length+byteArray.length); - newData.set(this.receiveBuffer); - newData.set(byteArray,this.receiveBuffer.length); - byteArray = newData; - delete this.receiveBuffer; - } - try { - var offset = 0; - while(offset < byteArray.length) { - var result = decodeMessage(byteArray,offset); - var wireMessage = result[0]; - offset = result[1]; - if (wireMessage !== null) { - messages.push(wireMessage); - } else { - break; - } - } - if (offset < byteArray.length) { - this.receiveBuffer = byteArray.subarray(offset); - } - } catch (error) { - var errorStack = ((error.hasOwnProperty("stack") == "undefined") ? error.stack.toString() : "No Error Stack Available"); - this._disconnected(ERROR.INTERNAL_ERROR.code , format(ERROR.INTERNAL_ERROR, [error.message,errorStack])); - return; - } - return messages; - }; - - ClientImpl.prototype._handleMessage = function(wireMessage) { - - this._trace("Client._handleMessage", wireMessage); - - try { - switch(wireMessage.type) { - case MESSAGE_TYPE.CONNACK: - this._connectTimeout.cancel(); - if (this._reconnectTimeout) - this._reconnectTimeout.cancel(); - - // If we have started using clean session then clear up the local state. - if (this.connectOptions.cleanSession) { - for (var key in this._sentMessages) { - var sentMessage = this._sentMessages[key]; - localStorage.removeItem("Sent:"+this._localKey+sentMessage.messageIdentifier); - } - this._sentMessages = {}; - - for (var key in this._receivedMessages) { - var receivedMessage = this._receivedMessages[key]; - localStorage.removeItem("Received:"+this._localKey+receivedMessage.messageIdentifier); - } - this._receivedMessages = {}; - } - // Client connected and ready for business. - if (wireMessage.returnCode === 0) { - - this.connected = true; - // Jump to the end of the list of uris and stop looking for a good host. - - if (this.connectOptions.uris) - this.hostIndex = this.connectOptions.uris.length; - - } else { - this._disconnected(ERROR.CONNACK_RETURNCODE.code , format(ERROR.CONNACK_RETURNCODE, [wireMessage.returnCode, CONNACK_RC[wireMessage.returnCode]])); - break; - } - - // Resend messages. - var sequencedMessages = []; - for (var msgId in this._sentMessages) { - if (this._sentMessages.hasOwnProperty(msgId)) - sequencedMessages.push(this._sentMessages[msgId]); - } - - // Also schedule qos 0 buffered messages if any - if (this._buffered_msg_queue.length > 0) { - var msg = null; - while ((msg = this._buffered_msg_queue.pop())) { - sequencedMessages.push(msg); - if (this.onMessageDelivered) - this._notify_msg_sent[msg] = this.onMessageDelivered(msg.payloadMessage); - } - } - - // Sort sentMessages into the original sent order. - var sequencedMessages = sequencedMessages.sort(function(a,b) {return a.sequence - b.sequence;} ); - for (var i=0, len=sequencedMessages.length; i - * Most applications will create just one Client object and then call its connect() method, - * however applications can create more than one Client object if they wish. - * In this case the combination of host, port and clientId attributes must be different for each Client object. - *

- * The send, subscribe and unsubscribe methods are implemented as asynchronous JavaScript methods - * (even though the underlying protocol exchange might be synchronous in nature). - * This means they signal their completion by calling back to the application, - * via Success or Failure callback functions provided by the application on the method in question. - * Such callbacks are called at most once per method invocation and do not persist beyond the lifetime - * of the script that made the invocation. - *

- * In contrast there are some callback functions, most notably onMessageArrived, - * that are defined on the {@link Paho.Client} object. - * These may get called multiple times, and aren't directly related to specific method invocations made by the client. - * - * @name Paho.Client - * - * @constructor - * - * @param {string} host - the address of the messaging server, as a fully qualified WebSocket URI, as a DNS name or dotted decimal IP address. - * @param {number} port - the port number to connect to - only required if host is not a URI - * @param {string} path - the path on the host to connect to - only used if host is not a URI. Default: '/mqtt'. - * @param {string} clientId - the Messaging client identifier, between 1 and 23 characters in length. - * - * @property {string} host - read only the server's DNS hostname or dotted decimal IP address. - * @property {number} port - read only the server's port. - * @property {string} path - read only the server's path. - * @property {string} clientId - read only used when connecting to the server. - * @property {function} onConnectionLost - called when a connection has been lost. - * after a connect() method has succeeded. - * Establish the call back used when a connection has been lost. The connection may be - * lost because the client initiates a disconnect or because the server or network - * cause the client to be disconnected. The disconnect call back may be called without - * the connectionComplete call back being invoked if, for example the client fails to - * connect. - * A single response object parameter is passed to the onConnectionLost callback containing the following fields: - *

    - *
  1. errorCode - *
  2. errorMessage - *
- * @property {function} onMessageDelivered - called when a message has been delivered. - * All processing that this Client will ever do has been completed. So, for example, - * in the case of a Qos=2 message sent by this client, the PubComp flow has been received from the server - * and the message has been removed from persistent storage before this callback is invoked. - * Parameters passed to the onMessageDelivered callback are: - *
    - *
  1. {@link Paho.Message} that was delivered. - *
- * @property {function} onMessageArrived - called when a message has arrived in this Paho.client. - * Parameters passed to the onMessageArrived callback are: - *
    - *
  1. {@link Paho.Message} that has arrived. - *
- * @property {function} onConnected - called when a connection is successfully made to the server. - * after a connect() method. - * Parameters passed to the onConnected callback are: - *
    - *
  1. reconnect (boolean) - If true, the connection was the result of a reconnect.
  2. - *
  3. URI (string) - The URI used to connect to the server.
  4. - *
- * @property {boolean} disconnectedPublishing - if set, will enable disconnected publishing in - * in the event that the connection to the server is lost. - * @property {number} disconnectedBufferSize - Used to set the maximum number of messages that the disconnected - * buffer will hold before rejecting new messages. Default size: 5000 messages - * @property {function} trace - called whenever trace is called. TODO - */ - var Client = function (host, port, path, clientId) { - - var uri; - - if (typeof host !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof host, "host"])); - - if (arguments.length == 2) { - // host: must be full ws:// uri - // port: clientId - clientId = port; - uri = host; - var match = uri.match(/^(wss?):\/\/((\[(.+)\])|([^\/]+?))(:(\d+))?(\/.*)$/); - if (match) { - host = match[4]||match[2]; - port = parseInt(match[7]); - path = match[8]; - } else { - throw new Error(format(ERROR.INVALID_ARGUMENT,[host,"host"])); - } - } else { - if (arguments.length == 3) { - clientId = path; - path = "/mqtt"; - } - if (typeof port !== "number" || port < 0) - throw new Error(format(ERROR.INVALID_TYPE, [typeof port, "port"])); - if (typeof path !== "string") - throw new Error(format(ERROR.INVALID_TYPE, [typeof path, "path"])); - - var ipv6AddSBracket = (host.indexOf(":") !== -1 && host.slice(0,1) !== "[" && host.slice(-1) !== "]"); - uri = "ws://"+(ipv6AddSBracket?"["+host+"]":host)+":"+port+path; - } - - var clientIdLength = 0; - for (var i = 0; i 65535) - throw new Error(format(ERROR.INVALID_ARGUMENT, [clientId, "clientId"])); - - var client = new ClientImpl(uri, host, port, path, clientId); - - //Public Properties - Object.defineProperties(this,{ - "host":{ - get: function() { return host; }, - set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } - }, - "port":{ - get: function() { return port; }, - set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } - }, - "path":{ - get: function() { return path; }, - set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } - }, - "uri":{ - get: function() { return uri; }, - set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } - }, - "clientId":{ - get: function() { return client.clientId; }, - set: function() { throw new Error(format(ERROR.UNSUPPORTED_OPERATION)); } - }, - "onConnected":{ - get: function() { return client.onConnected; }, - set: function(newOnConnected) { - if (typeof newOnConnected === "function") - client.onConnected = newOnConnected; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnected, "onConnected"])); - } - }, - "disconnectedPublishing":{ - get: function() { return client.disconnectedPublishing; }, - set: function(newDisconnectedPublishing) { - client.disconnectedPublishing = newDisconnectedPublishing; - } - }, - "disconnectedBufferSize":{ - get: function() { return client.disconnectedBufferSize; }, - set: function(newDisconnectedBufferSize) { - client.disconnectedBufferSize = newDisconnectedBufferSize; - } - }, - "onConnectionLost":{ - get: function() { return client.onConnectionLost; }, - set: function(newOnConnectionLost) { - if (typeof newOnConnectionLost === "function") - client.onConnectionLost = newOnConnectionLost; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnConnectionLost, "onConnectionLost"])); - } - }, - "onMessageDelivered":{ - get: function() { return client.onMessageDelivered; }, - set: function(newOnMessageDelivered) { - if (typeof newOnMessageDelivered === "function") - client.onMessageDelivered = newOnMessageDelivered; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageDelivered, "onMessageDelivered"])); - } - }, - "onMessageArrived":{ - get: function() { return client.onMessageArrived; }, - set: function(newOnMessageArrived) { - if (typeof newOnMessageArrived === "function") - client.onMessageArrived = newOnMessageArrived; - else - throw new Error(format(ERROR.INVALID_TYPE, [typeof newOnMessageArrived, "onMessageArrived"])); - } - }, - "trace":{ - get: function() { return client.traceFunction; }, - set: function(trace) { - if(typeof trace === "function"){ - client.traceFunction = trace; - }else{ - throw new Error(format(ERROR.INVALID_TYPE, [typeof trace, "onTrace"])); - } - } - }, - }); - - /** - * Connect this Messaging client to its server. - * - * @name Paho.Client#connect - * @function - * @param {object} connectOptions - Attributes used with the connection. - * @param {number} connectOptions.timeout - If the connect has not succeeded within this - * number of seconds, it is deemed to have failed. - * The default is 30 seconds. - * @param {string} connectOptions.userName - Authentication username for this connection. - * @param {string} connectOptions.password - Authentication password for this connection. - * @param {Paho.Message} connectOptions.willMessage - sent by the server when the client - * disconnects abnormally. - * @param {number} connectOptions.keepAliveInterval - the server disconnects this client if - * there is no activity for this number of seconds. - * The default value of 60 seconds is assumed if not set. - * @param {boolean} connectOptions.cleanSession - if true(default) the client and server - * persistent state is deleted on successful connect. - * @param {boolean} connectOptions.useSSL - if present and true, use an SSL Websocket connection. - * @param {object} connectOptions.invocationContext - passed to the onSuccess callback or onFailure callback. - * @param {function} connectOptions.onSuccess - called when the connect acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onSuccess method in the connectOptions. - *
- * @param {function} connectOptions.onFailure - called when the connect request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext as passed in to the onFailure method in the connectOptions. - *
  2. errorCode a number indicating the nature of the error. - *
  3. errorMessage text describing the error. - *
- * @param {array} connectOptions.hosts - If present this contains either a set of hostnames or fully qualified - * WebSocket URIs (ws://iot.eclipse.org:80/ws), that are tried in order in place - * of the host and port paramater on the construtor. The hosts are tried one at at time in order until - * one of then succeeds. - * @param {array} connectOptions.ports - If present the set of ports matching the hosts. If hosts contains URIs, this property - * is not used. - * @param {boolean} connectOptions.reconnect - Sets whether the client will automatically attempt to reconnect - * to the server if the connection is lost. - *
    - *
  • If set to false, the client will not attempt to automatically reconnect to the server in the event that the - * connection is lost.
  • - *
  • If set to true, in the event that the connection is lost, the client will attempt to reconnect to the server. - * It will initially wait 1 second before it attempts to reconnect, for every failed reconnect attempt, the delay - * will double until it is at 2 minutes at which point the delay will stay at 2 minutes.
  • - *
- * @param {number} connectOptions.mqttVersion - The version of MQTT to use to connect to the MQTT Broker. - *
    - *
  • 3 - MQTT V3.1
  • - *
  • 4 - MQTT V3.1.1
  • - *
- * @param {boolean} connectOptions.mqttVersionExplicit - If set to true, will force the connection to use the - * selected MQTT Version or will fail to connect. - * @param {array} connectOptions.uris - If present, should contain a list of fully qualified WebSocket uris - * (e.g. ws://iot.eclipse.org:80/ws), that are tried in order in place of the host and port parameter of the construtor. - * The uris are tried one at a time in order until one of them succeeds. Do not use this in conjunction with hosts as - * the hosts array will be converted to uris and will overwrite this property. - * @throws {InvalidState} If the client is not in disconnected state. The client must have received connectionLost - * or disconnected before calling connect for a second or subsequent time. - */ - this.connect = function (connectOptions) { - connectOptions = connectOptions || {} ; - validate(connectOptions, {timeout:"number", - userName:"string", - password:"string", - willMessage:"object", - keepAliveInterval:"number", - cleanSession:"boolean", - useSSL:"boolean", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - hosts:"object", - ports:"object", - reconnect:"boolean", - mqttVersion:"number", - mqttVersionExplicit:"boolean", - uris: "object"}); - - // If no keep alive interval is set, assume 60 seconds. - if (connectOptions.keepAliveInterval === undefined) - connectOptions.keepAliveInterval = 60; - - if (connectOptions.mqttVersion > 4 || connectOptions.mqttVersion < 3) { - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.mqttVersion, "connectOptions.mqttVersion"])); - } - - if (connectOptions.mqttVersion === undefined) { - connectOptions.mqttVersionExplicit = false; - connectOptions.mqttVersion = 4; - } else { - connectOptions.mqttVersionExplicit = true; - } - - //Check that if password is set, so is username - if (connectOptions.password !== undefined && connectOptions.userName === undefined) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.password, "connectOptions.password"])); - - if (connectOptions.willMessage) { - if (!(connectOptions.willMessage instanceof Message)) - throw new Error(format(ERROR.INVALID_TYPE, [connectOptions.willMessage, "connectOptions.willMessage"])); - // The will message must have a payload that can be represented as a string. - // Cause the willMessage to throw an exception if this is not the case. - connectOptions.willMessage.stringPayload = null; - - if (typeof connectOptions.willMessage.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_TYPE, [typeof connectOptions.willMessage.destinationName, "connectOptions.willMessage.destinationName"])); - } - if (typeof connectOptions.cleanSession === "undefined") - connectOptions.cleanSession = true; - if (connectOptions.hosts) { - - if (!(connectOptions.hosts instanceof Array) ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - if (connectOptions.hosts.length <1 ) - throw new Error(format(ERROR.INVALID_ARGUMENT, [connectOptions.hosts, "connectOptions.hosts"])); - - var usingURIs = false; - for (var i = 0; i - * @param {object} subscribeOptions - used to control the subscription - * - * @param {number} subscribeOptions.qos - the maximum qos of any publications sent - * as a result of making this subscription. - * @param {object} subscribeOptions.invocationContext - passed to the onSuccess callback - * or onFailure callback. - * @param {function} subscribeOptions.onSuccess - called when the subscribe acknowledgement - * has been received from the server. - * A single response object parameter is passed to the onSuccess callback containing the following fields: - *
    - *
  1. invocationContext if set in the subscribeOptions. - *
- * @param {function} subscribeOptions.onFailure - called when the subscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the subscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} subscribeOptions.timeout - which, if present, determines the number of - * seconds after which the onFailure calback is called. - * The presence of a timeout does not prevent the onSuccess - * callback from being called when the subscribe completes. - * @throws {InvalidState} if the client is not in connected state. - */ - this.subscribe = function (filter, subscribeOptions) { - if (typeof filter !== "string" && filter.constructor !== Array) - throw new Error("Invalid argument:"+filter); - subscribeOptions = subscribeOptions || {} ; - validate(subscribeOptions, {qos:"number", - invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (subscribeOptions.timeout && !subscribeOptions.onFailure) - throw new Error("subscribeOptions.timeout specified with no onFailure callback."); - if (typeof subscribeOptions.qos !== "undefined" && !(subscribeOptions.qos === 0 || subscribeOptions.qos === 1 || subscribeOptions.qos === 2 )) - throw new Error(format(ERROR.INVALID_ARGUMENT, [subscribeOptions.qos, "subscribeOptions.qos"])); - client.subscribe(filter, subscribeOptions); - }; - - /** - * Unsubscribe for messages, stop receiving messages sent to destinations described by the filter. - * - * @name Paho.Client#unsubscribe - * @function - * @param {string} filter - describing the destinations to receive messages from. - * @param {object} unsubscribeOptions - used to control the subscription - * @param {object} unsubscribeOptions.invocationContext - passed to the onSuccess callback - or onFailure callback. - * @param {function} unsubscribeOptions.onSuccess - called when the unsubscribe acknowledgement has been received from the server. - * A single response object parameter is passed to the - * onSuccess callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
- * @param {function} unsubscribeOptions.onFailure called when the unsubscribe request has failed or timed out. - * A single response object parameter is passed to the onFailure callback containing the following fields: - *
    - *
  1. invocationContext - if set in the unsubscribeOptions. - *
  2. errorCode - a number indicating the nature of the error. - *
  3. errorMessage - text describing the error. - *
- * @param {number} unsubscribeOptions.timeout - which, if present, determines the number of seconds - * after which the onFailure callback is called. The presence of - * a timeout does not prevent the onSuccess callback from being - * called when the unsubscribe completes - * @throws {InvalidState} if the client is not in connected state. - */ - this.unsubscribe = function (filter, unsubscribeOptions) { - if (typeof filter !== "string" && filter.constructor !== Array) - throw new Error("Invalid argument:"+filter); - unsubscribeOptions = unsubscribeOptions || {} ; - validate(unsubscribeOptions, {invocationContext:"object", - onSuccess:"function", - onFailure:"function", - timeout:"number" - }); - if (unsubscribeOptions.timeout && !unsubscribeOptions.onFailure) - throw new Error("unsubscribeOptions.timeout specified with no onFailure callback."); - client.unsubscribe(filter, unsubscribeOptions); - }; - - /** - * Send a message to the consumers of the destination in the Message. - * - * @name Paho.Client#send - * @function - * @param {string|Paho.Message} topic - mandatory The name of the destination to which the message is to be sent. - * - If it is the only parameter, used as Paho.Message object. - * @param {String|ArrayBuffer} payload - The message data to be sent. - * @param {number} qos The Quality of Service used to deliver the message. - *
- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- * @param {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - * @throws {InvalidState} if the client is not connected. - */ - this.send = function (topic,payload,qos,retained) { - var message ; - - if(arguments.length === 0){ - throw new Error("Invalid argument."+"length"); - - }else if(arguments.length == 1) { - - if (!(topic instanceof Message) && (typeof topic !== "string")) - throw new Error("Invalid argument:"+ typeof topic); - - message = topic; - if (typeof message.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); - client.send(message); - - }else { - //parameter checking in Message object - message = new Message(payload); - message.destinationName = topic; - if(arguments.length >= 3) - message.qos = qos; - if(arguments.length >= 4) - message.retained = retained; - client.send(message); - } - }; - - /** - * Publish a message to the consumers of the destination in the Message. - * Synonym for Paho.Mqtt.Client#send - * - * @name Paho.Client#publish - * @function - * @param {string|Paho.Message} topic - mandatory The name of the topic to which the message is to be published. - * - If it is the only parameter, used as Paho.Message object. - * @param {String|ArrayBuffer} payload - The message data to be published. - * @param {number} qos The Quality of Service used to deliver the message. - *
- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- * @param {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - * @throws {InvalidState} if the client is not connected. - */ - this.publish = function(topic,payload,qos,retained) { - var message ; - - if(arguments.length === 0){ - throw new Error("Invalid argument."+"length"); - - }else if(arguments.length == 1) { - - if (!(topic instanceof Message) && (typeof topic !== "string")) - throw new Error("Invalid argument:"+ typeof topic); - - message = topic; - if (typeof message.destinationName === "undefined") - throw new Error(format(ERROR.INVALID_ARGUMENT,[message.destinationName,"Message.destinationName"])); - client.send(message); - - }else { - //parameter checking in Message object - message = new Message(payload); - message.destinationName = topic; - if(arguments.length >= 3) - message.qos = qos; - if(arguments.length >= 4) - message.retained = retained; - client.send(message); - } - }; - - /** - * Normal disconnect of this Messaging client from its server. - * - * @name Paho.Client#disconnect - * @function - * @throws {InvalidState} if the client is already disconnected. - */ - this.disconnect = function () { - client.disconnect(); - }; - - /** - * Get the contents of the trace log. - * - * @name Paho.Client#getTraceLog - * @function - * @return {Object[]} tracebuffer containing the time ordered trace records. - */ - this.getTraceLog = function () { - return client.getTraceLog(); - }; - - /** - * Start tracing. - * - * @name Paho.Client#startTrace - * @function - */ - this.startTrace = function () { - client.startTrace(); - }; - - /** - * Stop tracing. - * - * @name Paho.Client#stopTrace - * @function - */ - this.stopTrace = function () { - client.stopTrace(); - }; - - this.isConnected = function() { - return client.connected; - }; - }; - - /** - * An application message, sent or received. - *

- * All attributes may be null, which implies the default values. - * - * @name Paho.Message - * @constructor - * @param {String|ArrayBuffer} payload The message data to be sent. - *

- * @property {string} payloadString read only The payload as a string if the payload consists of valid UTF-8 characters. - * @property {ArrayBuffer} payloadBytes read only The payload as an ArrayBuffer. - *

- * @property {string} destinationName mandatory The name of the destination to which the message is to be sent - * (for messages about to be sent) or the name of the destination from which the message has been received. - * (for messages received by the onMessage function). - *

- * @property {number} qos The Quality of Service used to deliver the message. - *

- *
0 Best effort (default). - *
1 At least once. - *
2 Exactly once. - *
- *

- * @property {Boolean} retained If true, the message is to be retained by the server and delivered - * to both current and future subscriptions. - * If false the server only delivers the message to current subscribers, this is the default for new Messages. - * A received message has the retained boolean set to true if the message was published - * with the retained boolean set to true - * and the subscrption was made after the message has been published. - *

- * @property {Boolean} duplicate read only If true, this message might be a duplicate of one which has already been received. - * This is only set on messages received from the server. - * - */ - var Message = function (newPayload) { - var payload; - if ( typeof newPayload === "string" || - newPayload instanceof ArrayBuffer || - (ArrayBuffer.isView(newPayload) && !(newPayload instanceof DataView)) - ) { - payload = newPayload; - } else { - throw (format(ERROR.INVALID_ARGUMENT, [newPayload, "newPayload"])); - } - - var destinationName; - var qos = 0; - var retained = false; - var duplicate = false; - - Object.defineProperties(this,{ - "payloadString":{ - enumerable : true, - get : function () { - if (typeof payload === "string") - return payload; - else - return parseUTF8(payload, 0, payload.length); - } - }, - "payloadBytes":{ - enumerable: true, - get: function() { - if (typeof payload === "string") { - var buffer = new ArrayBuffer(UTF8Length(payload)); - var byteStream = new Uint8Array(buffer); - stringToUTF8(payload, byteStream, 0); - - return byteStream; - } else { - return payload; - } - } - }, - "destinationName":{ - enumerable: true, - get: function() { return destinationName; }, - set: function(newDestinationName) { - if (typeof newDestinationName === "string") - destinationName = newDestinationName; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newDestinationName, "newDestinationName"])); - } - }, - "qos":{ - enumerable: true, - get: function() { return qos; }, - set: function(newQos) { - if (newQos === 0 || newQos === 1 || newQos === 2 ) - qos = newQos; - else - throw new Error("Invalid argument:"+newQos); - } - }, - "retained":{ - enumerable: true, - get: function() { return retained; }, - set: function(newRetained) { - if (typeof newRetained === "boolean") - retained = newRetained; - else - throw new Error(format(ERROR.INVALID_ARGUMENT, [newRetained, "newRetained"])); - } - }, - "topic":{ - enumerable: true, - get: function() { return destinationName; }, - set: function(newTopic) {destinationName=newTopic;} - }, - "duplicate":{ - enumerable: true, - get: function() { return duplicate; }, - set: function(newDuplicate) {duplicate=newDuplicate;} - } - }); - }; - - // Module contents. - return { - Client: Client, - Message: Message - }; - // eslint-disable-next-line no-nested-ternary - })(typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}); - return PahoMQTT; -}); \ No newline at end of file diff --git a/src/FLAD/lib/storage.js b/src/FLAD/lib/storage.js deleted file mode 100644 index 809ba35..0000000 --- a/src/FLAD/lib/storage.js +++ /dev/null @@ -1,10 +0,0 @@ -const storage = { - setItem: (key, item) => { - storage[key] = item; - }, - getItem: key => storage[key], - removeItem: key => { - delete storage[key]; - }, - }; - export default storage; \ No newline at end of file diff --git a/src/FLAD/models/Artist.ts b/src/FLAD/models/Artist.ts index 814e0ca..a06698a 100644 --- a/src/FLAD/models/Artist.ts +++ b/src/FLAD/models/Artist.ts @@ -1,15 +1,45 @@ export default class Artist { - private id: string; - private name: string; + private _id: string; + private _name: string; + private _image: string; private _url: string; - constructor(id: string, name: string, url: string) { - this.id = id; - this.name = name; + constructor(id: string, name: string, image: string, url: string) { + this._id = id; + this._name = name; + this._image = image; this._url = url; } + get id(): string { + return this._id; + } + + set id(value: string) { + this._id = value; + } + + get name(): string { + return this._name; + } + + set name(value: string) { + this._name = value; + } + + get image(): string { + return this._image; + } + + set image(value: string) { + this._image = value; + } + get url(): string { - return this.url; + return this._url; + } + + set url(value: string) { + this._url = value; } } \ No newline at end of file diff --git a/src/FLAD/models/Music.ts b/src/FLAD/models/Music.ts index b339e5b..f926eeb 100644 --- a/src/FLAD/models/Music.ts +++ b/src/FLAD/models/Music.ts @@ -1,15 +1,35 @@ +import Artist from "./Artist"; + export default class Music { private _id: string; - private _title: string; - private _bio: string; - private _image: string; + private _name: string; + private _url: string; + private _artists: Artist[]; + private _cover: string; + private _date: number; + private _duration: number; + private _explicit: boolean = false; private _trackPreviewUrl: string; - constructor(id: string, title: string, bio: string, image: string, trackPreviewUrl: string) { - this._title = title; - this._bio = bio; - this._image = image; + constructor( + id: string, + name: string, + url: string, + artists: Artist[], + cover: string, + date: number, + duration: number, + explicit: boolean, + trackPreviewUrl: string + ) { this._id = id; + this._name = name; + this._url = url; + this._artists = artists; + this._cover = cover; + this._date = date; + this._duration = duration; + this._explicit = explicit; this._trackPreviewUrl = trackPreviewUrl; } @@ -21,28 +41,60 @@ export default class Music { this._id = value; } - get title(): string { - return this._title; + get name(): string { + return this._name; + } + + set name(value: string) { + this._name = value; + } + + get url(): string { + return this._url; + } + + set url(value: string) { + this._url = value; + } + + get artists(): Artist[] { + return this._artists; } - set title(value: string) { - this._title = value; + set artists(value: Artist[]) { + this._artists = value; } - get bio(): string { - return this._bio; + get cover(): string { + return this._cover; } - set bio(value: string) { - this._bio = value; + set cover(value: string) { + this._cover = value; } - get image(): string { - return this._image; + get date(): number { + return this._date; } - set image(value: string) { - this._image = value; + set date(value: number) { + this._date = value; + } + + get duration(): number { + return this._duration; + } + + set duration(value: number) { + this._duration = value; + } + + get explicit(): boolean { + return this._explicit; + } + + set explicit(value: boolean) { + this._explicit = value; } get trackPreviewUrl(): string { @@ -53,3 +105,4 @@ export default class Music { this._trackPreviewUrl = value; } } + diff --git a/src/FLAD/models/MusicServiceProvider.ts b/src/FLAD/models/MusicServiceProvider.ts new file mode 100644 index 0000000..9cfd902 --- /dev/null +++ b/src/FLAD/models/MusicServiceProvider.ts @@ -0,0 +1,15 @@ +import EmptyMusicService from "../services/EmptyMusicService"; +import IMusicService from "../services/musics/interfaces/IMusicService"; +import SpotifyService from "../services/musics/spotify/SpotifyService"; + +export class MusicServiceProvider { + static musicService: IMusicService; + + static initSpotify(refreshToken: string, idSpotify: string) { + this.musicService = new SpotifyService(refreshToken, idSpotify); + } + + static resetService() { + this.musicService = new EmptyMusicService(); + } +} \ No newline at end of file diff --git a/src/FLAD/models/Person.ts b/src/FLAD/models/Person.ts new file mode 100644 index 0000000..3473eff --- /dev/null +++ b/src/FLAD/models/Person.ts @@ -0,0 +1,19 @@ +export class Person { + private _id: string; + private _name: string; + public image: string; + + constructor(id: string, idSpotify: string, name: string, email: string, creationDate: Date, image: string) { + this._id = id; + this._name = name; + this.image = image; + } + + get id(): string { + return this._id; + } + + get name(): string { + return this._name; + } +} \ No newline at end of file diff --git a/src/FLAD/models/Spot.ts b/src/FLAD/models/Spot.ts index af2b426..24b0b5e 100644 --- a/src/FLAD/models/Spot.ts +++ b/src/FLAD/models/Spot.ts @@ -1,20 +1,23 @@ import Music from "./Music"; +import { Person } from "./Person"; export class Spot { - private _userId: string; + private _user: string; private _music: Music; + private _date: Date; - constructor(userId: string, music: Music) { - this._userId = userId; + constructor(userId: string, music: Music, date: Date) { + this._user = userId; this._music = music; + this._date = date; } get userSpotifyId(): string { - return this._userId; + return this._user; } set userSpotifyId(value: string) { - this._userId = value; + this._user = value; } get music(): Music { @@ -24,4 +27,12 @@ export class Spot { set music(value: Music) { this._music = value; } + + get date(): Date { + return this._date; + } + + set date(value: Date) { + this._date = value; + } } \ No newline at end of file diff --git a/src/FLAD/models/mapper/ArtistMapper.ts b/src/FLAD/models/mapper/ArtistMapper.ts new file mode 100644 index 0000000..ea06e9f --- /dev/null +++ b/src/FLAD/models/mapper/ArtistMapper.ts @@ -0,0 +1,7 @@ +import Artist from "../Artist"; + +export default class ArtistMapper { + static toModel(artist: any): Artist { + return new Artist(artist.id, artist.name, (artist?.images?.[0]?.url ?? ""), artist.external_urls.spotify); + } +} \ No newline at end of file diff --git a/src/FLAD/models/mapper/MusicMapper.ts b/src/FLAD/models/mapper/MusicMapper.ts index 8b8260b..1a5ce0e 100644 --- a/src/FLAD/models/mapper/MusicMapper.ts +++ b/src/FLAD/models/mapper/MusicMapper.ts @@ -1,12 +1,18 @@ import Music from "../Music"; +import ArtistMapper from "./ArtistMapper"; export default class MusicMapper { static toModel(music: any): Music { + const artists = music.artists.map((artist: any) => ArtistMapper.toModel(artist)); return new Music( music.id, music.name, - music.artists[0].name, + music.external_urls.spotify, + artists, music.album.images[0].url, + music.album.release_date.split('-')[0], + music.duration_ms / 1000, + music.explicit, music.preview_url ); } diff --git a/src/FLAD/models/mapper/SpotMapper.ts b/src/FLAD/models/mapper/SpotMapper.ts new file mode 100644 index 0000000..8a2c257 --- /dev/null +++ b/src/FLAD/models/mapper/SpotMapper.ts @@ -0,0 +1,7 @@ +import { Spot } from "../Spot"; + +export class SpotMapper { + public static toModel(spot: any): Spot { + return new Spot(spot.idUser, spot.music, new Date(spot.date)); + } +} \ No newline at end of file diff --git a/src/FLAD/navigation/AuthNavigation.tsx b/src/FLAD/navigation/AuthNavigation.tsx index 0f7bf68..417160e 100644 --- a/src/FLAD/navigation/AuthNavigation.tsx +++ b/src/FLAD/navigation/AuthNavigation.tsx @@ -16,17 +16,17 @@ export default function AuthNavigation() { const [appIsReady, setAppIsReady] = useState(false); const dispatch = useDispatch(); - async function prepare() { - //@ts-ignore - await dispatch(getRefreshToken()) - } - async function check() { if (tokenProcessed && appIsReady) { await SplashScreen.hideAsync(); } } + async function prepare() { + //@ts-ignore + dispatch(getRefreshToken()) + } + async function initDarkMode() { const currentValue: string | null = await AsyncStorage.getItem('dark'); if (currentValue) { diff --git a/src/FLAD/navigation/FavoriteNavigation.tsx b/src/FLAD/navigation/FavoriteNavigation.tsx index 4f2f0ef..fdb2b3f 100644 --- a/src/FLAD/navigation/FavoriteNavigation.tsx +++ b/src/FLAD/navigation/FavoriteNavigation.tsx @@ -6,16 +6,16 @@ import { createSharedElementStackNavigator } from 'react-navigation-shared-eleme const Stack = createSharedElementStackNavigator(); export default function MusicNavigation() { return ( - + { return [route.params.music.id] }} + options={{ headerShown: false }} /> ) diff --git a/src/FLAD/navigation/HomeNavigation.tsx b/src/FLAD/navigation/HomeNavigation.tsx index c92640b..6c66b20 100644 --- a/src/FLAD/navigation/HomeNavigation.tsx +++ b/src/FLAD/navigation/HomeNavigation.tsx @@ -1,10 +1,9 @@ -import React, { useEffect, useState } from 'react'; -import { View, StyleSheet, Platform } from 'react-native'; +import React, { useEffect } from 'react'; +import { View, StyleSheet, Platform, Alert } from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { NavigationContainer } from '@react-navigation/native'; import FavoriteNavigation from './FavoriteNavigation'; import SettingNavigation from './SettingNavigation'; - import normalize from '../components/Normalize'; // @ts-ignore import FontAwesome from 'react-native-vector-icons/FontAwesome'; @@ -13,89 +12,18 @@ import MessagingNavigation from './MessagingNavigation'; import { useDispatch, useSelector } from 'react-redux'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; -import { getCurrentUserMusic, getSpotList } from '../redux/thunk/spotThunk'; -import SpotifyService from '../services/spotify/spotify.service'; -import * as SecureStore from 'expo-secure-store'; -import * as Location from 'expo-location'; -import axios from 'axios'; -import qs from 'qs'; +import { getUserCurrentMusic } from '../redux/thunk/appThunk'; +import { logout } from '../redux/thunk/authThunk'; +import { setAccessError, setErrorEmptyMusic } from '../redux/actions/userActions'; -const MY_SECURE_AUTH_STATE_KEY = 'MySecureAuthStateKeySpotify'; export default function HomeNavigation() { - const [setErrorMsg] = useState(''); //@ts-ignore - const tokenSend: string = useSelector(state => state.userReducer.userFladToken); + const favoritesMusicLength = useSelector(state => state.appReducer.nbAddedFavoritesMusic); //@ts-ignore - const currentMusic: Music = useSelector(state => state.appReducer.userCurrentMusic); - - const dispatch = useDispatch(); - - const requestLocationPermission = async () => { - const { status } = await Location.requestForegroundPermissionsAsync(); - if (status !== 'granted') { - console.log('Permission to access location was denied'); - } else { - console.log('Permission to access location was granted'); - } - } - - useEffect(() => { - requestLocationPermission(); - const sendLocationUpdate = async () => { - try { - - let tmpKey: string = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY); - //@ts-ignore - dispatch(getCurrentUserMusic(new SpotifyService(tmpKey))) - let { status } = await Location.requestForegroundPermissionsAsync(); - if (status == 'granted') { - // should app is ready - const locationresp = await Location.getCurrentPositionAsync({}); - // send location to server - if (currentMusic) { - const body: Record = { - longitude: locationresp.coords.longitude, - latitude: locationresp.coords.latitude, - currentMusic: currentMusic.id - } - const resp = await axios({ - url: 'https://flad-api-production.up.railway.app/api/users/nextTo?' + qs.stringify(body), - method: 'GET', - headers: { - Authorization: `Bearer ${tokenSend}`, - }, - }); - const datat: Record = resp.data.listUser2; - //@ts-ignore - dispatch(getSpotList(datat, new SpotifyService(tmpKey))) - } - else { - return; - } - - - } - else { - //@ts-ignore - let { status } = Location.requestForegroundPermissionsAsync(); - if (status !== 'granted') { - setErrorMsg('Permission to access location was denied'); - return; - } - return; - - } - } catch (error) { - console.log(error); - } - }; - const interval = setInterval(sendLocationUpdate, 30000); - return () => { - clearInterval(interval); - }; - }, [currentMusic]); - + const accessError = useSelector(state => state.userReducer.accessError); + //@ts-ignore + const errorEmptyMusic = useSelector(state => state.userReducer.errorEmptyMusic); // @ts-ignore const isDark = useSelector(state => state.userReducer.dark); const style = isDark ? colorsDark : colorsLight; @@ -109,8 +37,51 @@ export default function HomeNavigation() { text: 'rgb(138, 138, 138)', } }; - //@ts-ignore - const favoritesMusicLength: number = useSelector(state => state.appReducer.favoriteMusic.length); + + const dispatch = useDispatch(); + + useEffect(() => { + //@ts-ignore + dispatch(getUserCurrentMusic()); + }, []); + + useEffect(() => { + if (accessError) { + Alert.alert( + "Problème lié à votre compte", + "Votre compte ne fait plus partie des utilisateurs ayant accès à l'application. Pour plus d'informations, veuillez contacter l'équipe de support à l'adresse suivante : fladdevpro@gmail.com.", + [ + { + text: 'Réessayer plus tard', + onPress: () => { + dispatch(setAccessError(false)) + //@ts-ignore + dispatch(logout()); + }, + }, + ], + { cancelable: false } + ); + } + }, [accessError]); + + useEffect(() => { + if (errorEmptyMusic) { + Alert.alert( + "Bienvenue sur FLAD 🎵", + "Votre compte Spotify semble tout neuf, donc pour le moment, vous ne pouvez pas encore partager de musique.\n\n" + + "Pas encore de playlist secrète ? Aucun morceau honteux ? Nous attendons impatiemment vos découvertes musicales !", + [ + { + text: "D'accord", + onPress: () => dispatch(setErrorEmptyMusic(false)), + } + ] + ); + } + }, [errorEmptyMusic]); + + return ( // @ts-ignore diff --git a/src/FLAD/navigation/MessagingNavigation.tsx b/src/FLAD/navigation/MessagingNavigation.tsx index db11753..2a9ef55 100644 --- a/src/FLAD/navigation/MessagingNavigation.tsx +++ b/src/FLAD/navigation/MessagingNavigation.tsx @@ -1,23 +1,21 @@ import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; import ConversationScreen from '../screens/ConversationScreen' import ChatScreen from '../screens/ChatScreen'; +import { createStackNavigator } from '@react-navigation/stack'; export default function MessagingNavigation() { const Stack = createStackNavigator(); - return ( - + ) diff --git a/src/FLAD/navigation/SpotNavigation.tsx b/src/FLAD/navigation/SpotNavigation.tsx index 23d5b65..1abaeba 100644 --- a/src/FLAD/navigation/SpotNavigation.tsx +++ b/src/FLAD/navigation/SpotNavigation.tsx @@ -1,27 +1,21 @@ import React from 'react'; -import { createStackNavigator } from '@react-navigation/stack'; import SpotScreen from '../screens/SpotScreen' import DetailScreen from '../screens/DetailScreen'; - +import { createStackNavigator } from '@react-navigation/stack'; export default function SpotNavigation() { const Stack = createStackNavigator(); - return ( - + ) diff --git a/src/FLAD/package.json b/src/FLAD/package.json index 589354b..6869128 100644 --- a/src/FLAD/package.json +++ b/src/FLAD/package.json @@ -36,7 +36,6 @@ "react-native-reanimated": "~2.12.0", "react-native-safe-area-context": "4.4.1", "react-native-screens": "~3.18.0", - "react-native-shared-element": "0.8.4", "react-native-svg": "13.4.0", "react-native-vector-icons": "^9.2.0", "react-navigation-shared-element": "^3.1.3", diff --git a/src/FLAD/redux/actions/appActions.ts b/src/FLAD/redux/actions/appActions.ts index 8f4ee12..2e530c5 100644 --- a/src/FLAD/redux/actions/appActions.ts +++ b/src/FLAD/redux/actions/appActions.ts @@ -1,13 +1,22 @@ import Music from "../../models/Music"; +import { Spot } from "../../models/Spot"; import { favoritesTypes } from "../types/favoritesTypes"; +import { spotifyTypes } from "../types/spotifyTypes"; -export const getFavoritesMusic = (music: Music[]) => { +export const setUserCurrentMusic = (music: Music | null) => { return { - type: favoritesTypes.GET_FAVORITE_MUSICS, + type: spotifyTypes.GET_USER_CURRENT_MUSIC, payload: music, }; } +export const setFavoriteMusic = (spots: Spot[]) => { + return { + type: favoritesTypes.GET_FAVORITE_MUSICS, + payload: spots, + }; +} + export const addFavoritesMusic = (music: Music) => { return { type: favoritesTypes.ADD_FAVORITE_MUSICS, diff --git a/src/FLAD/redux/actions/spotActions.tsx b/src/FLAD/redux/actions/spotActions.tsx index 0ed80df..213f1ec 100644 --- a/src/FLAD/redux/actions/spotActions.tsx +++ b/src/FLAD/redux/actions/spotActions.tsx @@ -1,6 +1,5 @@ import Music from "../../models/Music"; import { Spot } from "../../models/Spot"; -import { spotifyTypes } from "../types/spotifyTypes"; import { spotTypes } from "../types/spotTypes"; export const setSpotList = (spotList: Spot[]) => { @@ -9,6 +8,7 @@ export const setSpotList = (spotList: Spot[]) => { payload: spotList, }; } + export const removeFromSpotList = (spot: Spot) => { return { type: spotTypes.REMOVE_SPOT, @@ -21,11 +21,4 @@ export const addSpotListMock = (spotList: Spot[]) => { type: spotTypes.ADD_SPOT_MOCK, payload: spotList, }; -} - -export const setUserCurrentMusic = (currentMusic: Music) => { - return { - type: spotifyTypes.GET_USER_CURRENT_MUSIC, - payload: currentMusic, - }; } \ No newline at end of file diff --git a/src/FLAD/redux/actions/userActions.tsx b/src/FLAD/redux/actions/userActions.tsx index d1bc54b..e8ec712 100644 --- a/src/FLAD/redux/actions/userActions.tsx +++ b/src/FLAD/redux/actions/userActions.tsx @@ -26,13 +26,6 @@ export const restoreToken = () => { }; } -export const userSignUp = (user: User) => { - return { - type: userTypes.SIGNUP, - payload: user - }; -} - export const userLogout = () => { return { type: userTypes.USER_LOGOUT, @@ -41,28 +34,56 @@ export const userLogout = () => { export const setDarkMode = (value: boolean) => { return { - type: userTypes.DARK_MODE, + type: userTypes.SET_DARK_MODE, payload: value }; } export const setErrorLogin = (value: boolean) => { return { - type: userTypes.ERROR_LOGIN, + type: userTypes.SET_ERROR_LOGIN, payload: value }; } export const setErrorSignup = (value: string) => { return { - type: userTypes.ERROR_SIGNUP, + type: userTypes.SET_ERROR_SIGNUP, payload: value }; } export const setErrorNetwork = (value: boolean) => { return { - type: userTypes.ERROR_NETWORK, + type: userTypes.SET_ERROR_NETWORK, + payload: value + }; +} + +export const setErrorEmptyMusic = (value: boolean) => { + return { + type: userTypes.SET_ERROR_EMPTY_MUSIC, + payload: value + }; +} + +export const setAccessError = (value: boolean) => { + return { + type: userTypes.SET_ERROR_ACCESS, + payload: value + }; +} + +export const setErrorUpdate = (value: boolean) => { + return { + type: userTypes.SET_ERROR_UPDATE, + payload: value + }; +} + +export const setErrorUpdateMessage = (value: string) => { + return { + type: userTypes.SET_ERROR_UPDATE_MESSAGE, payload: value }; } \ No newline at end of file diff --git a/src/FLAD/redux/reducers/appReducer.tsx b/src/FLAD/redux/reducers/appReducer.tsx index f42066e..864e97b 100644 --- a/src/FLAD/redux/reducers/appReducer.tsx +++ b/src/FLAD/redux/reducers/appReducer.tsx @@ -1,21 +1,19 @@ -import { spotsData } from "../../data/data"; -import { discoveriesTypes } from "../types/discoverieTypes"; +import { Spot } from "../../models/Spot"; import { favoritesTypes } from "../types/favoritesTypes"; import { spotifyTypes } from "../types/spotifyTypes"; import { spotTypes } from "../types/spotTypes"; const initialState = { - spot: spotsData, - favoriteMusic: [], - userCurrentMusic: null + spot: [] as Spot[], + favoriteMusic: [] as Spot[], + userCurrentMusic: null, + nbAddedFavoritesMusic: 0 } const appReducer = (state = initialState, action: any) => { switch (action.type) { case favoritesTypes.GET_FAVORITE_MUSICS: return { ...state, favoriteMusic: action.payload }; - case favoritesTypes.ADD_FAVORITE_MUSICS: - return { ...state, favoriteMusic: [action.payload, ...state.favoriteMusic] }; case favoritesTypes.REMOVE_FAVORITE_MUSICS: return { ...state, favoriteMusic: state.favoriteMusic }; case spotTypes.FETCH_SPOT: @@ -26,8 +24,6 @@ const appReducer = (state = initialState, action: any) => { return { ...state, spot: updatedSpotList }; case spotTypes.REMOVE_SPOT: return { ...state, spot: state.spot.filter((spot) => spot.userSpotifyId !== action.payload.userSpotifyId && spot.music.id !== action.payload.music.id) }; - case discoveriesTypes.FETCH_DISCOVERIES: - return; case spotifyTypes.GET_USER_CURRENT_MUSIC: return { ...state, userCurrentMusic: action.payload }; default: diff --git a/src/FLAD/redux/reducers/userReducer.tsx b/src/FLAD/redux/reducers/userReducer.tsx index e99e138..874aa54 100644 --- a/src/FLAD/redux/reducers/userReducer.tsx +++ b/src/FLAD/redux/reducers/userReducer.tsx @@ -10,7 +10,11 @@ const initialState = { failedSignup: false, errorMessage: null, errorNetwork: false, - dark: null + dark: null, + errorEmptyMusic: false, + accessError: false, + errorUpdateMessage: null, + errorUpdate: false, } const userReducer = (state = initialState, action: any) => { @@ -29,15 +33,6 @@ const userReducer = (state = initialState, action: any) => { failedSignup: false, errorNetwork: false }; - case userTypes.SIGNUP: - return { - ...state, - user: action.payload, - isLogedIn: true, - failedLogin: false, - failedSignup: false, - errorNetwork: false - }; case userTypes.USER_LOGOUT: AsyncStorage.removeItem('dark'); return { @@ -51,14 +46,22 @@ const userReducer = (state = initialState, action: any) => { ...state, userSpotifyToken: action.payload }; - case userTypes.ERROR_LOGIN: + case userTypes.SET_ERROR_LOGIN: return { ...state, failedLogin: action.payload } - case userTypes.ERROR_SIGNUP: + case userTypes.SET_ERROR_SIGNUP: return { ...state, failedSignup: true, errorMessage: action.payload } - case userTypes.DARK_MODE: + case userTypes.SET_DARK_MODE: return { ...state, dark: action.payload } - case userTypes.ERROR_NETWORK: + case userTypes.SET_ERROR_NETWORK: return { ...state, errorNetwork: action.payload } + case userTypes.SET_ERROR_EMPTY_MUSIC: + return { ...state, errorEmptyMusic: action.payload } + case userTypes.SET_ERROR_ACCESS: + return { ...state, accessError: action.payload } + case userTypes.SET_ERROR_UPDATE: + return { ...state, errorUpdate: action.payload } + case userTypes.SET_ERROR_UPDATE_MESSAGE: + return { ...state, errorUpdateMessage: action.payload, errorUpdate: true } default: return state; } diff --git a/src/FLAD/redux/store.tsx b/src/FLAD/redux/store.tsx index b115d3d..99b5547 100644 --- a/src/FLAD/redux/store.tsx +++ b/src/FLAD/redux/store.tsx @@ -17,7 +17,7 @@ const store = configureStore({ reducer: reducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ serializableCheck: { - ignoredActions: [spotTypes.FETCH_SPOT, spotifyTypes.GET_USER_CURRENT_MUSIC, favoritesTypes.ADD_FAVORITE_MUSICS, favoritesTypes.REMOVE_FAVORITE_MUSICS, spotTypes.REMOVE_SPOT, userTypes.LOGIN, userTypes.SIGNUP ], + ignoredActions: [spotTypes.FETCH_SPOT, spotifyTypes.GET_USER_CURRENT_MUSIC, favoritesTypes.ADD_FAVORITE_MUSICS, favoritesTypes.REMOVE_FAVORITE_MUSICS, spotTypes.REMOVE_SPOT, userTypes.LOGIN], ignoredActionPaths: ['appReducer'], ignoredPaths: ['appReducer', 'userReducer'] } diff --git a/src/FLAD/redux/thunk/appThunk.tsx b/src/FLAD/redux/thunk/appThunk.tsx new file mode 100644 index 0000000..fb49b75 --- /dev/null +++ b/src/FLAD/redux/thunk/appThunk.tsx @@ -0,0 +1,81 @@ +import axios from "axios"; +import * as SecureStore from 'expo-secure-store'; +import { Spot } from "../../models/Spot"; +import configs from "../../constants/config"; +import { MusicServiceProvider } from "../../models/MusicServiceProvider"; +import { setFavoriteMusic, setUserCurrentMusic } from "../actions/appActions"; +import { setAccessError, setErrorEmptyMusic } from "../actions/userActions"; +import { SpotMapper } from "../../models/mapper/SpotMapper"; + +export const getUserCurrentMusic = () => { + //@ts-ignore + return async dispatch => { + try { + let idTrack; + const resp = await MusicServiceProvider.musicService.getCurrentlyPlayingMusic(); + if (resp === null) { + idTrack = await MusicServiceProvider.musicService.getRecentlyPlayedMusic(); + if (idTrack === null) { + dispatch(setErrorEmptyMusic(true)); + dispatch(setUserCurrentMusic(null)); + return; + } + } else { + idTrack = resp; + } + const music = await MusicServiceProvider.musicService.getMusicById(idTrack); + dispatch(setUserCurrentMusic(music)) + } catch (error: any) { + console.error("Error retrieving music currently listened : " + error); + switch (error.response.status) { + case 403: + dispatch(setAccessError(true)); + break; + default: + dispatch(setAccessError(true)); + break; + } + } + } +} + +export const addFavoriteMusic = (spot: Spot) => { + //@ts-ignore + return async dispatch => { + + }; +} + +export const getFavoriteMusic = () => { + //@ts-ignore + return async dispatch => { + try { + let token: string | null = await SecureStore.getItemAsync(configs.key); + const headers = { + 'Authorization': 'Bearer ' + token + }; + const resp = await axios.get( + configs.API_URL + '/user/musics', + { headers } + ) + + const musicIds = resp.data.musics.map((music: any) => music.idMusic); + const musics = await MusicServiceProvider.musicService.getMusicsWithIds(musicIds); + const result = resp.data.musics + .filter((music: any) => musics.some((m: any) => m.id === music.idMusic)) + .map((music: any) => { + const matchingMusic = musics.find((m: any) => m.id === music.idMusic); + return { + ...music, + music: matchingMusic, + }; + }); + + dispatch(setFavoriteMusic(result.map((item: any) => SpotMapper.toModel(item)))); + + } catch (error: any) { + console.error(error); + dispatch(setAccessError(true)); + } + }; +} \ No newline at end of file diff --git a/src/FLAD/redux/thunk/authThunk.tsx b/src/FLAD/redux/thunk/authThunk.tsx index 6f29987..9c29c03 100644 --- a/src/FLAD/redux/thunk/authThunk.tsx +++ b/src/FLAD/redux/thunk/authThunk.tsx @@ -1,10 +1,10 @@ import axios from "axios"; import configs from "../../constants/config"; -import { LoginCredentials, RegisterCredentials, restoreToken, userLogin, userLogout, userSignUp, setErrorLogin, setErrorSignup, setErrorNetwork } from "../actions/userActions"; +import { LoginCredentials, RegisterCredentials, restoreToken, userLogin, userLogout, setErrorLogin, setErrorSignup, setErrorNetwork } from "../actions/userActions"; import * as SecureStore from 'expo-secure-store'; import { UserMapper } from "../../models/mapper/UserMapper"; +import { MusicServiceProvider } from "../../models/MusicServiceProvider"; -const key = 'userToken'; const keyRemember = 'rememberUser'; export const register = (resgisterCredential: RegisterCredentials) => { @@ -22,7 +22,7 @@ export const register = (resgisterCredential: RegisterCredentials) => { config ) const token = resp.data.token; - await SecureStore.setItemAsync(key, token); + await SecureStore.setItemAsync(configs.key, token); await SecureStore.setItemAsync(keyRemember, 'true'); const headers = { 'Authorization': 'Bearer ' + token @@ -31,8 +31,8 @@ export const register = (resgisterCredential: RegisterCredentials) => { configs.API_URL + '/user', { headers } ) - dispatch(userSignUp(UserMapper.toModel(user.data.data))); - + MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify); + dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { console.error("Error : " + error.message); switch (error.response.status) { @@ -70,7 +70,7 @@ export const login = (loginCredential: LoginCredentials, remember: boolean) => { ) const token = resp.data.token; - await SecureStore.setItemAsync(key, token); + await SecureStore.setItemAsync(configs.key, token); if (remember) { await SecureStore.setItemAsync(keyRemember, remember.toString()); } @@ -83,8 +83,8 @@ export const login = (loginCredential: LoginCredentials, remember: boolean) => { configs.API_URL + '/user', { headers } ) + MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify); dispatch(userLogin(UserMapper.toModel(user.data.data))); - } catch (error: any) { console.error("Error : " + error.message); switch (error.response.status) { @@ -103,7 +103,7 @@ export const getRefreshToken = () => { //@ts-ignore return async dispatch => { let remember: string | null = await SecureStore.getItemAsync(keyRemember); - let token: string | null = await SecureStore.getItemAsync(key); + let token: string | null = await SecureStore.getItemAsync(configs.key); if (token) { if (remember) { const headers = { @@ -114,14 +114,13 @@ export const getRefreshToken = () => { configs.API_URL + '/user', { headers } ) + MusicServiceProvider.initSpotify(user.data.data.tokenSpotify, user.data.data.idSpotify); await dispatch(userLogin(UserMapper.toModel(user.data.data))); } catch (error: any) { - await SecureStore.deleteItemAsync(key); - dispatch(userLogout()); + dispatch(logout()); } } else { - await SecureStore.deleteItemAsync(key); - dispatch(userLogout()); + dispatch(logout()); } } dispatch(restoreToken()); @@ -132,7 +131,7 @@ export const getRefreshToken = () => { export const deleteUser = () => { //@ts-ignore return async dispatch => { - let token: string | null = await SecureStore.getItemAsync(key); + let token: string | null = await SecureStore.getItemAsync(configs.key); if (token) { const headers = { 'Authorization': 'Bearer ' + token @@ -142,8 +141,7 @@ export const deleteUser = () => { configs.API_URL + '/user', { headers } ) - await SecureStore.deleteItemAsync(key); - dispatch(userLogout()); + dispatch(logout()); } catch (error: any) { console.error("Error deleting account : " + error.message); } @@ -154,8 +152,9 @@ export const deleteUser = () => { export const logout = () => { //@ts-ignore return async dispatch => { - await SecureStore.deleteItemAsync(key); + await SecureStore.deleteItemAsync(configs.key); await SecureStore.deleteItemAsync(keyRemember); + MusicServiceProvider.resetService(); dispatch(userLogout()); } } \ No newline at end of file diff --git a/src/FLAD/redux/thunk/socialThunk.tsx b/src/FLAD/redux/thunk/socialThunk.tsx deleted file mode 100644 index fbb323c..0000000 --- a/src/FLAD/redux/thunk/socialThunk.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import axios from "axios"; -import { Spot } from "../../models/Spot"; - -export const likeSpot = async (spot: Spot) => { - return async (dispatch) => { - axios.post("osdj").then(responce => { - if (responce.status == 200) { - dispatch(true); - } - }).catch(error => { - console.log("something goes wrong while searching : " + error); - ; - }) - - - }; -} \ No newline at end of file diff --git a/src/FLAD/redux/thunk/spotThunk.tsx b/src/FLAD/redux/thunk/spotThunk.tsx index f576c33..72c1659 100644 --- a/src/FLAD/redux/thunk/spotThunk.tsx +++ b/src/FLAD/redux/thunk/spotThunk.tsx @@ -1,69 +1,9 @@ import axios from "axios"; -import * as SecureStore from 'expo-secure-store'; -import { Spot } from "../../models/Spot"; -import SpotifyService from "../../services/spotify/spotify.service"; -import { setSpotList, setUserCurrentMusic } from "../actions/spotActions"; const key = 'userToken'; -export type CreateSpotReqBody = { - id: string; - name: string; - artist: string; - linkCover: string; - user: string; -} -export const getSpotList = (spotsData : Record , resuestHandler: SpotifyService) => { +export const getSpotList = () => { //@ts-ignore return async dispatch => { - try { - //@ts-ignore - if (spotsData) { - - const spots = await Promise.all( - Object.entries(spotsData).map(async ([userId, value]) => { - const completeMusic = await resuestHandler.getMusicById(value); - return new Spot(userId, completeMusic); - }) - ); - dispatch(setSpotList(spots)); // our action is called here - } else { - console.log('Login Failed', 'Username or Password is incorrect'); - } - - } catch (error) { - console.log('Error---------', error); - } } -} -export const getCurrentUserMusic = (resuestHandler: SpotifyService) => { - //@ts-ignore - return async dispatch => { - try { - //@ts-ignore - var currentTrackResponse = await resuestHandler.getUserCurrentMusic(); - if (!currentTrackResponse) { - const recentlyTrackResponse = await resuestHandler.getUserRecentlyPlayedMusic(); - if (!recentlyTrackResponse) { - throw new Error; - } else { - currentTrackResponse = recentlyTrackResponse; - } - } - const completeMusic = await resuestHandler.getMusicById(currentTrackResponse); - if(!completeMusic){ - return; - } - dispatch(setUserCurrentMusic(completeMusic)); - } - catch (error) { - console.log('Error---------', error); - } - } -} -export const searchMusic = async (resuestHandler: SpotifyService, search: string) => { - - return async (dispatch) => { - return resuestHandler.searchMusic(search).then(musics => dispatch((musics))).catch(err => console.log("something goes wrong while searching : " + err)); - }; -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/FLAD/redux/thunk/userThunk.tsx b/src/FLAD/redux/thunk/userThunk.tsx index 7dbb7c4..369b225 100644 --- a/src/FLAD/redux/thunk/userThunk.tsx +++ b/src/FLAD/redux/thunk/userThunk.tsx @@ -1,8 +1,72 @@ -import { setDarkMode } from "../actions/userActions"; +import axios from "axios"; +import configs from "../../constants/config"; +import { setDarkMode, setErrorNetwork, setErrorUpdateMessage, userLogin } from "../actions/userActions"; +import * as SecureStore from 'expo-secure-store'; +import { UserMapper } from "../../models/mapper/UserMapper"; export const darkMode = (value: boolean) => { //@ts-ignore return async dispatch => { dispatch(setDarkMode(value)); } +} + +export const setName = (name: string) => { + //@ts-ignore + return async dispatch => { + try { + let token: string | null = await SecureStore.getItemAsync(configs.key); + const headers = { + 'Authorization': 'Bearer ' + token + }; + await axios.put(configs.API_URL + '/user/name', { name }, { headers }); + + const user = await axios.get( + configs.API_URL + '/user', + { headers } + ) + dispatch(userLogin(UserMapper.toModel(user.data.data))); + + } catch (error: any) { + console.error("Error : " + error.message); + switch (error.response.status) { + case 409: + dispatch(setErrorUpdateMessage("Nom déjà utilisé.")) + break; + default: + dispatch(setErrorNetwork(true)); + break; + } + } + } +} + +export const setMail = (email: string) => { + //@ts-ignore + return async dispatch => { + try { + let token: string | null = await SecureStore.getItemAsync(configs.key); + const headers = { + 'Authorization': 'Bearer ' + token + }; + + await axios.put(configs.API_URL + '/user/email', { email }, { headers }); + + const user = await axios.get( + configs.API_URL + '/user', + { headers } + ) + dispatch(userLogin(UserMapper.toModel(user.data.data))); + } catch (error: any) { + console.error("Error : " + error.message); + switch (error.response.status) { + case 409: + dispatch(setErrorUpdateMessage("Email déjà utilisé.")) + break; + default: + dispatch(setErrorNetwork(true)); + break; + } + } + } } \ No newline at end of file diff --git a/src/FLAD/redux/types/playlistTypes.tsx b/src/FLAD/redux/types/playlistTypes.tsx index 4d8c3e4..c22f6a6 100644 --- a/src/FLAD/redux/types/playlistTypes.tsx +++ b/src/FLAD/redux/types/playlistTypes.tsx @@ -1,5 +1,3 @@ export const playlistTypes = { - FETCH_USER_PLAYLISTS: 'FETCH_SPOT', SAVE_IN_FLAD_PLAYLIST: 'SAVE_IN_FLAD_PLAYLIST', - FETCH_FLAD_PLAYLIST: 'FETCH_SPOT', } \ No newline at end of file diff --git a/src/FLAD/redux/types/spotTypes.tsx b/src/FLAD/redux/types/spotTypes.tsx index 8a8f70b..f865c98 100644 --- a/src/FLAD/redux/types/spotTypes.tsx +++ b/src/FLAD/redux/types/spotTypes.tsx @@ -1,5 +1,5 @@ export const spotTypes = { FETCH_SPOT: 'FETCH_SPOT', ADD_SPOT_MOCK: 'ADD_SPOT_MOCK', - REMOVE_SPOT: 'REMOVE_SPOT', + REMOVE_SPOT: 'REMOVE_SPOT' } \ No newline at end of file diff --git a/src/FLAD/redux/types/spotifyTypes.ts b/src/FLAD/redux/types/spotifyTypes.ts index 0b84ebe..a6e9a79 100644 --- a/src/FLAD/redux/types/spotifyTypes.ts +++ b/src/FLAD/redux/types/spotifyTypes.ts @@ -1,3 +1,3 @@ export const spotifyTypes = { - GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC', + GET_USER_CURRENT_MUSIC: 'GET_USER_CURRENT_MUSIC' } \ No newline at end of file diff --git a/src/FLAD/redux/types/userTypes.tsx b/src/FLAD/redux/types/userTypes.tsx index 69dee1b..8e5eb34 100644 --- a/src/FLAD/redux/types/userTypes.tsx +++ b/src/FLAD/redux/types/userTypes.tsx @@ -1,12 +1,14 @@ export const userTypes = { RESTORE_TOKEN: "RESTORE_TOKEN", LOGIN: 'LOGIN', - SIGNUP: 'SIGNUP', USER_LOGOUT: 'USER_LOGOUT', SAVE_SPOTIFY: 'SAVE_SPOTIFY', - UPDATE_USER: 'UPDATE_USER', - ERROR_LOGIN: "ERROR_LOGIN", - ERROR_SIGNUP: "ERROR_SIGNUP", - DARK_MODE: "DARK_MODE", - ERROR_NETWORK: "ERROR_NETWORK" + SET_ERROR_LOGIN: "SET_ERROR_LOGIN", + SET_ERROR_SIGNUP: "SET_ERROR_SIGNUP", + SET_DARK_MODE: "SET_DARK_MODE", + SET_ERROR_NETWORK: "SET_ERROR_NETWORK", + SET_ERROR_ACCESS: "SET_ERROR_ACCESS", + SET_ERROR_EMPTY_MUSIC: "SET_ERROR_EMPTY_MUSIC", + SET_ERROR_UPDATE: "SET_ERROR_UPDATE", + SET_ERROR_UPDATE_MESSAGE: "SET_ERROR_UPDATE_MESSAGE" } \ No newline at end of file diff --git a/src/FLAD/screens/ChatScreen.tsx b/src/FLAD/screens/ChatScreen.tsx index 2e8cd6c..07ecbb9 100644 --- a/src/FLAD/screens/ChatScreen.tsx +++ b/src/FLAD/screens/ChatScreen.tsx @@ -2,21 +2,21 @@ import { useNavigation } from "@react-navigation/native"; import React, { useEffect } from "react"; import { GiftedChat } from "react-native-gifted-chat"; -export default function ChatScreen() { +export default function Chat() { - const navigation = useNavigation(); + const navigation = useNavigation(); - useEffect(() => { - navigation.getParent()?.setOptions({ - tabBarStyle: { - display: "none" - } - }); - return () => navigation.getParent()?.setOptions({ - tabBarStyle: undefined - }); - }, [navigation]); - return ( - - ) + useEffect(() => { + navigation.getParent()?.setOptions({ + tabBarStyle: { + display: "none" + } + }); + return () => navigation.getParent()?.setOptions({ + tabBarStyle: undefined + }); + }, [navigation]); + return ( + + ) } \ No newline at end of file diff --git a/src/FLAD/screens/ConversationScreen.tsx b/src/FLAD/screens/ConversationScreen.tsx index c93c062..d260dc9 100644 --- a/src/FLAD/screens/ConversationScreen.tsx +++ b/src/FLAD/screens/ConversationScreen.tsx @@ -14,10 +14,10 @@ export default function ConversationScreen() { const navigation = useNavigation(); const friends = [ - { id: 1, name: "Lucas", lastMessage: "J'en ai marre de provot", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, - { id: 2, name: "Louison", lastMessage: "Tu vien piscine ?", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, + { id: 1, name: "Lucas", lastMessage: "J'en ai marre de provot", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" }, + { id: 2, name: "Louison", lastMessage: "Tu vien piscine ?", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" }, { id: 3, name: "Dave", lastMessage: "Ok c noté !", source: "https://img.lemde.fr/2019/04/05/0/0/960/960/664/0/75/0/18299d3_tUvp2AZPH_jnsIL2ypVFGUro.jpg" }, - { id: 4, name: "Valentin", lastMessage: "Haha react native c incroyable !!!", source: "https://yt3.googleusercontent.com/CgPFZUSWbFj9txLG_8l48YRCwnrlfQya8sw_UCB-s3NGkQEnLj--KZI0CqSCyP2XqPfOB-j9yQ=s900-c-k-c0x00ffffff-no-rj" }, + { id: 4, name: "Valentin", lastMessage: "Haha react native c incroyable !!!", source: "https://i1.sndcdn.com/artworks-ncJnbnDbNOFd-0-t500x500.jpg" }, ]; const style = isDark ? colorsDark : colorsLight; diff --git a/src/FLAD/screens/DetailScreen.tsx b/src/FLAD/screens/DetailScreen.tsx index 40efbac..48e6b4f 100644 --- a/src/FLAD/screens/DetailScreen.tsx +++ b/src/FLAD/screens/DetailScreen.tsx @@ -1,24 +1,20 @@ import { useNavigation } from "@react-navigation/native"; -import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Pressable } from "react-native"; +import { View, Text, Image, StyleSheet, TouchableOpacity, ScrollView, Pressable, Share, Alert } from "react-native"; import Animated, { interpolate, SensorType, useAnimatedSensor, useAnimatedStyle, withSpring } from "react-native-reanimated"; - import { Audio } from 'expo-av'; import { useEffect, useState } from "react"; import normalize from '../components/Normalize'; import Music from "../models/Music"; -import SpotifyService from "../services/spotify/spotify.service"; import { LinearGradient } from "expo-linear-gradient"; -import FontAwesome from 'react-native-vector-icons/FontAwesome'; import { Feather as Icon } from "@expo/vector-icons"; -import HorizontalFlatList from "../components/HorizontalFlatListComponent"; -import * as SecureStore from 'expo-secure-store'; -import { MY_SECURE_AUTH_STATE_KEY } from "./RegisterScreen"; +import { MusicServiceProvider } from "../models/MusicServiceProvider"; +import { HorizontalFlatList } from "../components/HorizontalFlatList"; +import { LittleCard } from "../components/littleCard"; const halfPi = Math.PI / 2; - //@ts-ignore -const DetailScreen = ({ route }) => { +export default function DetailScreen({ route }) { const music: Music = route.params.music; const [currentspot] = useState(music); const [simularMusic, setSimularMusic] = useState([]); @@ -27,19 +23,13 @@ const DetailScreen = ({ route }) => { const navigator = useNavigation(); - + useEffect(() => { getSimilarTrack(); }, []); const getSimilarTrack = async () => { - try { - let token = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY); - const service = new SpotifyService(token); - const simularMusic = await service.getSimilarTrack(currentspot.id, 5, 'FR'); - setSimularMusic(simularMusic); - } catch (error) { - console.error('Error ================ in getSimilarTrack', error); - } + const simularMusic = await MusicServiceProvider.musicService.getSimilarTracks(currentspot.id); + setSimularMusic(simularMusic); } const handlePlaySound = async () => { @@ -48,7 +38,7 @@ const DetailScreen = ({ route }) => { { uri: music.trackPreviewUrl }, { shouldPlay: true } ); - setSound(newSound); + //setSound(newSound); setIsPlaying(true); } else { @@ -75,6 +65,21 @@ const DetailScreen = ({ route }) => { : undefined; }, [sound]); + const onShare = async () => { + try { + const result = await Share.share({ + message: + music.url, + }); + } catch (error: any) { + Alert.alert(error.message); + } + }; + + const addToPlaylist = async () => { + MusicServiceProvider.musicService.addToPlaylist(music.id); + }; + const sensor = useAnimatedSensor(SensorType.ROTATION); const styleAniamatedImage = useAnimatedStyle(() => { const { pitch, roll } = sensor.sensor.value; @@ -101,7 +106,7 @@ const DetailScreen = ({ route }) => { blurRadius={133} style={styles.back_drop} source={{ - uri: currentspot.image, + uri: currentspot.cover, }} > { { - + }}> - @@ -151,14 +155,13 @@ const DetailScreen = ({ route }) => { - - Dans ma collection - @@ -166,7 +169,7 @@ const DetailScreen = ({ route }) => { {/* */} Partager cette music - + {simularMusic.length !== 0 && ( @@ -174,12 +177,13 @@ const DetailScreen = ({ route }) => { { // @ts-ignore - navigator.replace("DetailsSpot", { "music": props }) }} > + navigator.replace("Detail", { "music": props }) }} > )} )} + @@ -187,8 +191,6 @@ const DetailScreen = ({ route }) => { ); }; -export default DetailScreen; - const styles = StyleSheet.create({ mainSafeArea: { flex: 1, diff --git a/src/FLAD/screens/FavoriteScreen.tsx b/src/FLAD/screens/FavoriteScreen.tsx index 6a4db9f..a149b46 100644 --- a/src/FLAD/screens/FavoriteScreen.tsx +++ b/src/FLAD/screens/FavoriteScreen.tsx @@ -1,15 +1,18 @@ -import React from 'react'; -import { StyleSheet, Text, View, FlatList, TouchableHighlight, SafeAreaView } from 'react-native'; +import React, { useEffect } from 'react'; +import { StyleSheet, Text, View, FlatList, SafeAreaView } from 'react-native'; import CardMusic from '../components/CardMusicComponent'; import normalize from '../components/Normalize'; -import Music from '../models/Music'; import { Svg, Path } from 'react-native-svg'; import FladyComponent from '../components/FladyComponent'; import { useNavigation } from "@react-navigation/native"; import { useSelector } from 'react-redux'; -import { SharedElement } from 'react-navigation-shared-element'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; +import { useDispatch } from 'react-redux'; +import { getFavoriteMusic } from '../redux/thunk/appThunk'; +import { Spot } from '../models/Spot'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import Artist from '../models/Artist'; export default function FavoriteScreen() { @@ -17,26 +20,31 @@ export default function FavoriteScreen() { const isDark = useSelector(state => state.userReducer.dark); const style = isDark ? colorsDark : colorsLight; - const navigation = useNavigation(); - //@ts-ignore - const favoritesMusic = useSelector(state => state.appReducer.favoriteMusic); const images = [ { id: 1, source: require('../assets/images/flady_love.png') }, { id: 2, source: require('../assets/images/flady_star.png') }, { id: 3, source: require('../assets/images/flady_angry.png') }, { id: 4, source: require('../assets/images/flady_cry.png') }, ]; - const navigueToDetail = (music: any) => { - // @ts-ignore - navigation.navigate("Detail", { "music": music }) - }; + const navigation = useNavigation(); + //@ts-ignore + const favoriteMusic = useSelector(state => state.appReducer.favoriteMusic); + + const dispatch = useDispatch(); + + useEffect(() => { + //@ts-ignore + dispatch(getFavoriteMusic()) + }, []); + + const styles = StyleSheet.create({ mainSafeArea: { flex: 1, backgroundColor: style.body, }, titleContainer: { - marginTop: 10, + marginVertical: 10, marginLeft: 20, }, header: { @@ -55,30 +63,6 @@ export default function FavoriteScreen() { fontSize: normalize(20), color: '#787878', marginBottom: 5 - }, - button: { - marginTop: '10%', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - alignSelf: 'center', - backgroundColor: 'white', - width: normalize(100), - height: normalize(100), - borderRadius: 21 - }, - buttonImage: { - width: normalize(46), - height: normalize(46), - }, - shadow: { - shadowColor: '#000', - shadowOffset: { - width: 2, - height: 3, - }, - shadowOpacity: 0.50, - shadowRadius: 3.84, } }); @@ -95,15 +79,14 @@ export default function FavoriteScreen() { Retrouvez ici vos musiques favorites item.music.id} renderItem={({ item }) => ( - { navigueToDetail(item) }}> - - - - + //@ts-ignore + { navigation.navigate("Detail", { "music": item.music }) }}> + artist.name).join(', ')} id={item.music.id} /> + )} - keyExtractor={(item: Music) => item.title} ListFooterComponent={ <> What's your mood? diff --git a/src/FLAD/screens/LoginScreen.tsx b/src/FLAD/screens/LoginScreen.tsx index 97181a2..ca56920 100644 --- a/src/FLAD/screens/LoginScreen.tsx +++ b/src/FLAD/screens/LoginScreen.tsx @@ -1,12 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity } from 'react-native'; -import { setErrorNetwork } from "../redux/actions/userActions"; +import { setErrorNetwork, LoginCredentials } from "../redux/actions/userActions"; import { useNavigation } from "@react-navigation/native"; import normalize from '../components/Normalize'; import { login } from '../redux/thunk/authThunk'; import { useDispatch, useSelector } from 'react-redux'; import { Audio } from 'expo-av'; -import { LoginCredentials } from '../redux/actions/userActions'; // @ts-ignore const DismissKeyboard = ({ children }) => ( diff --git a/src/FLAD/screens/ProfilScreen.tsx b/src/FLAD/screens/ProfilScreen.tsx index 3a49b9a..79d97d7 100644 --- a/src/FLAD/screens/ProfilScreen.tsx +++ b/src/FLAD/screens/ProfilScreen.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Alert, View, Text, StyleSheet, TouchableWithoutFeedback, Keyboard, ScrollView, Image } from 'react-native'; import { TextInput, TouchableOpacity } from 'react-native-gesture-handler'; import { Svg, Path } from 'react-native-svg'; @@ -11,6 +11,8 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; import { deleteUser } from '../redux/thunk/authThunk'; +import { setMail, setName } from '../redux/thunk/userThunk'; +import { setErrorUpdate } from '../redux/actions/userActions'; // @ts-ignore const DismissKeyboard = ({ children }) => ( @@ -23,7 +25,13 @@ export default function ProfilScreen() { // @ts-ignore const isDark = useSelector(state => state.userReducer.dark); // @ts-ignore + const errorUpdateMessage = useSelector(state => state.userReducer.errorUpdateMessage); + // @ts-ignore + const errorUpdate = useSelector(state => state.userReducer.errorUpdate); + // @ts-ignore const userCurrent = useSelector(state => state.userReducer.user); + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); const style = isDark ? colorsDark : colorsLight; const navigation = useNavigation(); const [isModalVisible, setIsModalVisible] = React.useState(false); @@ -60,6 +68,90 @@ export default function ProfilScreen() { }); }; + const submitUsername = () => { + const isUsernameValid = /^\w+$/.test(username); + + if (username.length > 30) { + Alert.alert("Erreur modification", "Le nom d'utilisateur ne peut pas être plus grand que 30 caractères."); + return; + } + if (!isUsernameValid) { + Alert.alert("Erreur modification", "Le nom d'utilisateur ne peut pas posséder de caractères spéciaux."); + return; + } + Alert.alert( + 'Confirmation', + 'Êtes-vous sûr de vouloir changer de nom d\'utilisateur ?', + [ + { + text: 'Annuler', + style: 'cancel' + }, + { + text: 'Oui', + onPress: () => { + //@ts-ignore + dispatch(setName(username)); + setUsername(""); + }, + style: 'destructive' + }, + ], + { cancelable: false } + ); + } + + const submitEmail = () => { + const isEmailValid = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(email); + + if (email.length > 100) { + Alert.alert("Erreur modification", "L'adresse e-mail ne peut pas être plus grand que 100 caractères."); + return; + } + if (!isEmailValid) { + Alert.alert("Erreur modification", "L'adresse e-mail n\'est pas valide."); + return; + } + Alert.alert( + 'Confirmation', + 'Êtes-vous sûr de vouloir changer l\'adresse e-mail ?', + [ + { + text: 'Annuler', + style: 'cancel' + }, + { + text: 'Oui', + onPress: () => { + //@ts-ignore + dispatch(setMail(email)); + setEmail(""); + }, + style: 'destructive' + }, + ], + { cancelable: false } + ); + } + + useEffect(() => { + if (errorUpdate) { + Alert.alert( + "Erreur modification", + errorUpdateMessage, + [ + { + text: 'Ok', + onPress: () => { + dispatch(setErrorUpdate(false)) + }, + }, + ], + { cancelable: false } + ); + } + }, [errorUpdate]); + const styles = StyleSheet.create({ mainSafeArea: { flex: 1, @@ -170,15 +262,22 @@ export default function ProfilScreen() { flexDirection: 'row', }, textInputId: { + marginLeft: 50, + width: '50%', + color: style.Text, + fontSize: normalize(18), + }, + textIdSpotify: { marginLeft: 50, width: '57%', color: style.Text, + fontWeight: 'bold', fontSize: normalize(18), }, textInputMail: { marginLeft: 100, color: style.Text, - width: '57%', + width: '50%', fontSize: normalize(18) }, passwordOption: { @@ -201,18 +300,20 @@ export default function ProfilScreen() { marginTop: 5 }, cancelText: { - fontSize: normalize(20), + fontSize: normalize(18), color: '#1c77fb' }, updateText: { marginLeft: 60, - fontSize: normalize(20), + fontWeight: 'bold', + fontSize: normalize(18), color: '#404040' }, titlePassword: { - fontSize: normalize(22), + fontWeight: 'bold', + fontSize: normalize(20), color: style.Text, - marginLeft: 50 + marginLeft: 65 }, warning: { color: '#98989f', @@ -286,13 +387,29 @@ export default function ProfilScreen() { + + Id. Spotify + {userCurrent.idSpotify} + Identifiant - + + {username.length >= 5 && ( + submitUsername()}> + + + )} Mail - + + {email.length >= 7 && ( + submitEmail()}> + + + )} diff --git a/src/FLAD/screens/RegisterScreen.tsx b/src/FLAD/screens/RegisterScreen.tsx index b3bf15e..0877e9b 100644 --- a/src/FLAD/screens/RegisterScreen.tsx +++ b/src/FLAD/screens/RegisterScreen.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity, Platform } from 'react-native'; +import { Alert, View, Image, StyleSheet, Text, ImageBackground, TextInput, TouchableWithoutFeedback, Keyboard, TouchableOpacity } from 'react-native'; import { useNavigation } from "@react-navigation/native"; import normalize from '../components/Normalize'; import * as AuthSession from 'expo-auth-session'; @@ -7,8 +7,6 @@ import { register } from '../redux/thunk/authThunk'; import { useDispatch, useSelector } from 'react-redux'; import { Audio } from 'expo-av'; import { RegisterCredentials } from '../redux/actions/userActions'; -import { setSpotList } from '../redux/actions/spotActions'; -import { spotsData } from '../data/data'; import configs from '../constants/config'; // @ts-ignore @@ -39,14 +37,14 @@ export default function RegisterScreen() { await sound.playAsync(); } - function addMockSpots() { - dispatch(setSpotList(spotsData)) - } - const submitForm = () => { - const isUsernameValid = /^[a-zA-Z0-9_]+$/.test(username); - const isEmailValid = /^[a-zA-Z0-9_]+@[a-zA-Z0-9_]+\.[^\s@]+$/.test(email); + const isUsernameValid = /^\w+$/.test(username); + const isEmailValid = /^\w+@\w+\.[^\s@]+$/.test(email); + if (username.length > 30) { + Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas être plus grand que 30 caractères."); + return; + } if (username == "" || username == null) { Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas être vide."); return; @@ -55,6 +53,10 @@ export default function RegisterScreen() { Alert.alert("Erreur inscription", "Le nom d'utilisateur ne peut pas posséder de caractères spéciaux."); return; } + if (email.length > 100) { + Alert.alert("Erreur inscription", "L'adresse e-mail ne peut pas être plus grand que 100 caractères."); + return; + } if (!isEmailValid) { Alert.alert("Erreur inscription", "L'adresse e-mail n\'est pas valide."); return; @@ -78,7 +80,6 @@ export default function RegisterScreen() { //@ts-ignore dispatch(register(credentials)) - addMockSpots() playSound() } diff --git a/src/FLAD/screens/SettingScreen.tsx b/src/FLAD/screens/SettingScreen.tsx index 1b1d373..9020662 100644 --- a/src/FLAD/screens/SettingScreen.tsx +++ b/src/FLAD/screens/SettingScreen.tsx @@ -12,6 +12,7 @@ import { darkMode } from '../redux/thunk/userThunk'; import { colorsDark } from '../constants/colorsDark'; import { colorsLight } from '../constants/colorsLight'; import { User } from '../models/User'; +import Artist from '../models/Artist'; // @ts-ignore const DismissKeyboard = ({ children }) => ( @@ -54,7 +55,7 @@ export default function SettingScreen() { const Deconnection = () => { //@ts-ignore - dispatch(logout()) + dispatch(logout()); } const [isCheckedLocalisation, setIsCheckedLocalisation] = useState(false); @@ -234,11 +235,18 @@ export default function SettingScreen() { marginBottom: 5 }, mascot: { - width: normalize(90), - height: normalize(90), + width: normalize(70), + height: normalize(70), position: 'absolute', right: normalize(0), - top: normalize(10) + top: normalize(20) + }, + creationDateText: { + marginTop: 10, + fontSize: normalize(13), + fontWeight: '700', + color: style.Text, + opacity: 0.4 } }) @@ -268,7 +276,7 @@ export default function SettingScreen() { - {currentUser.name} + {currentUser.name.charAt(0).toUpperCase() + currentUser.name.slice(1)} id. Spotify, mail et mot de passe @@ -328,7 +336,7 @@ export default function SettingScreen() { - + artist.name).join(', ')} id='1' /> @@ -345,6 +353,15 @@ export default function SettingScreen() { Se deconnecter + + Compte créer le {currentUser.creationDate.toLocaleString('fr-FR', { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + })} + diff --git a/src/FLAD/screens/SpotScreen.tsx b/src/FLAD/screens/SpotScreen.tsx index 4e22a78..77b4916 100644 --- a/src/FLAD/screens/SpotScreen.tsx +++ b/src/FLAD/screens/SpotScreen.tsx @@ -4,18 +4,15 @@ import { LinearGradient } from 'expo-linear-gradient'; import * as Haptics from 'expo-haptics'; import Animated from 'react-native-reanimated'; import Card from '../components/Card'; -import AdjustSize from '../components/AdjustSize'; import normalize from '../components/Normalize'; import LottieView from 'lottie-react-native' import Lotties from '../assets/lottie/Lottie'; import Loading from '../components/LoadingComponent'; import { useNavigation } from '@react-navigation/native'; -import Music from '../model/Music'; import { addFavoritesMusic } from '../redux/actions/appActions'; import { useDispatch, useSelector } from 'react-redux'; -import { Spot } from '../model/Spot'; +import { Spot } from '../models/Spot'; import { removeFromSpotList, setSpotList } from '../redux/actions/spotActions'; -import { spotsData } from '../data/data'; export default function SpotScreen() { //@ts-ignore @@ -89,7 +86,7 @@ export default function SpotScreen() { alignItems: "center", }} source={{ - uri: currentCard.music.image, + uri: currentCard.music.cover, }} > @@ -100,9 +97,9 @@ export default function SpotScreen() { left: wWidht / 9, top: normalize(87), color: "#FFFFFF", - fontSize: normalize(AdjustSize(currentCard.music.title)), + fontSize: normalize(currentCard.music.name), fontWeight: "800", - }}>{currentCard.music.title} + }}>{currentCard.music.name} {currentCard.music.bio} + }}>{currentCard.music.artists[0].name} @@ -118,11 +115,11 @@ export default function SpotScreen() { {cards.map((card) => ( - + { hapti(card) }} > { onSwipe(direction) }} /> diff --git a/src/FLAD/services/EmptyMusicService.ts b/src/FLAD/services/EmptyMusicService.ts new file mode 100644 index 0000000..e277f98 --- /dev/null +++ b/src/FLAD/services/EmptyMusicService.ts @@ -0,0 +1,26 @@ +import Music from "../models/Music"; +import IMusicService from "./musics/interfaces/IMusicService"; + +export default class EmptyMusicService implements IMusicService { + getMusicById(id: string): Promise { + throw new Error("Method not implemented."); + } + getRecentlyPlayedMusic(): Promise { + throw new Error("Method not implemented."); + } + getCurrentlyPlayingMusic(): Promise { + throw new Error("Method not implemented."); + } + getMusicsWithIds(ids: string[]): Promise { + throw new Error("Method not implemented."); + } + getMusicsWithName(name: string): Promise { + throw new Error("Method not implemented."); + } + addToPlaylist(idTrack: string): void { + throw new Error("Method not implemented."); + } + getSimilarTracks(idTrack: string): Promise { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/FLAD/services/musics/interfaces/IMusicService.ts b/src/FLAD/services/musics/interfaces/IMusicService.ts new file mode 100644 index 0000000..5f6cd72 --- /dev/null +++ b/src/FLAD/services/musics/interfaces/IMusicService.ts @@ -0,0 +1,11 @@ +import Music from "../../../models/Music"; + +export default interface IMusicService { + getMusicById(id: string): Promise; + getRecentlyPlayedMusic(): Promise; + getCurrentlyPlayingMusic(): Promise; + getMusicsWithIds(ids: string[]): Promise; + getMusicsWithName(name: string): Promise; + addToPlaylist(idTrack: string): void; + getSimilarTracks(idTrack: string): Promise; +} \ No newline at end of file diff --git a/src/FLAD/services/musics/spotify/SpotifyService.ts b/src/FLAD/services/musics/spotify/SpotifyService.ts new file mode 100644 index 0000000..74c8a57 --- /dev/null +++ b/src/FLAD/services/musics/spotify/SpotifyService.ts @@ -0,0 +1,189 @@ +import axios from "axios"; +import Music from "../../../models/Music"; +import IMusicService from "../interfaces/IMusicService"; +import TokenSpotify from "./TokenSpotify"; +import MusicMapper from "../../../models/mapper/MusicMapper"; + +export default class SpotifyService implements IMusicService { + private readonly API_URL = "https://api.spotify.com/v1"; + private readonly PLAYLIST_NAME = "Flad's discovery"; + private _token: TokenSpotify; + private _idSpotify: string; + + constructor(refreshToken: string, idSpotify: string) { + this._token = new TokenSpotify(refreshToken); + this._idSpotify = idSpotify; + } + + async getMusicById(id: string): Promise { + const access_token = await this._token.getAccessToken(); + try { + const response = await axios.get(`${this.API_URL}/tracks/${id}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + return MusicMapper.toModel(response.data) + } catch (error: any) { + console.log("Error retrieving music information : " + error) + throw new Error("Error retrieving music information : " + error) + } + } + + async getRecentlyPlayedMusic(limit: number = 1): Promise { + const access_token = await this._token.getAccessToken(); + + const response = await axios.get(`${this.API_URL}/me/player/recently-played?limit=${limit}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + if (response.data.items.length === 0) { + return null; + } + return response.data.items[0].track.id + } + + async getCurrentlyPlayingMusic(): Promise { + const access_token = await this._token.getAccessToken(); + + const response = await axios.get(`${this.API_URL}/me/player/currently-playing`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + + if (response.data.item === undefined) { + return null; + } + return response.data.item.id + } + + async getMusicsWithIds(ids: string[]): Promise { + const access_token = await this._token.getAccessToken(); + var url = `${this.API_URL}/tracks?market=FR&ids=`; + if (ids.length == 0) { + return []; + } + + url += ids.join('%2C'); + + try { + const response = await axios.get(url, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + const tracksData = response.data.tracks.filter((musicData: any) => musicData !== null); + return tracksData.map((musicData: any) => MusicMapper.toModel(musicData)); + } catch (error: any) { + console.log(error) + return []; + } + } + + async getMusicsWithName(name: string, limit: number = 20, offset: number = 0): Promise { + const access_token = await this._token.getAccessToken(); + + try { + const response = await axios.get(`${this.API_URL}/search?q=track%3A${name}&type=track&market=fr&limit=${limit}&offset=${offset}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + return response.data.tracks.items.map((musicData: any) => MusicMapper.toModel(musicData)); + } catch (error: any) { + console.log(error) + return []; + } + } + + async _getPlaylistId(): Promise { + const access_token = await this._token.getAccessToken(); + + const headers = { + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json' + }; + const response = await axios.get(`${this.API_URL}/me/playlists?limit=50`, { headers }); + const fladPlaylist = response.data.items.filter((playlist: any) => playlist.name === this.PLAYLIST_NAME); + + if (fladPlaylist.length >= 1) { + return fladPlaylist[0].id; + } + + return await this._createPlaylist(); + } + + async _createPlaylist(): Promise { + const access_token = await this._token.getAccessToken(); + const data = { + name: this.PLAYLIST_NAME, + description: 'Retrouvez toutes vos découvertes faites sur FladMusic 🎵', + public: true + }; + var headers = { + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }; + const response = await axios.post(`${this.API_URL}/users/${this._idSpotify}/playlists`, data, { headers }); + + return response.data.id; + } + + async _isInPlaylist(idTrack: string, idPlaylist: string): Promise { + const access_token = await this._token.getAccessToken(); + const response = await axios.get(`${this.API_URL}/playlists/${idPlaylist}/tracks?limit=100`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + var nbTracks = response.data.items.filter((item: any) => item.track.id === idTrack).length; + + return (nbTracks >= 1) ? true : false; + } + + async addToPlaylist(idTrack: string): Promise { + var idPlaylist = await this._getPlaylistId(); + + if (await this._isInPlaylist(idTrack, idPlaylist)) { + return; + } + const access_token = await this._token.getAccessToken(); + + const data = { + uris: [`spotify:track:${idTrack}`], + position: 0 + }; + const headers = { + 'Authorization': `Bearer ${access_token}`, + 'Content-Type': 'application/json', + }; + + await axios.post(`${this.API_URL}/playlists/${idPlaylist}/tracks`, data, { headers }) + .then(response => { + console.log('Song successfully added to playlist.'); + }) + .catch(error => { + console.error('Error adding song to playlist: ', error); + }); + } + + async getSimilarTracks(idTrack: string, limit: number = 20, offset: number = 0): Promise { + const access_token = await this._token.getAccessToken(); + + try { + const response = await axios.get(`${this.API_URL}/recommendations?limit=${limit}&offset=${offset}&seed_tracks=${idTrack}`, { + headers: { + 'Authorization': `Bearer ${access_token}` + }, + }); + return response.data.tracks.map((musicData: any) => MusicMapper.toModel(musicData)); + } catch (error: any) { + console.log(error) + return []; + } + } + +} \ No newline at end of file diff --git a/src/FLAD/services/musics/spotify/TokenSpotify.ts b/src/FLAD/services/musics/spotify/TokenSpotify.ts new file mode 100644 index 0000000..42afe85 --- /dev/null +++ b/src/FLAD/services/musics/spotify/TokenSpotify.ts @@ -0,0 +1,41 @@ +import axios from "axios"; +import configs from "../../../constants/config"; + +export default class TokenSpotify { + private _accessToken: string = ''; + private _refreshToken: string; + private _tokenEnd: Date; + + constructor(refreshToken: string) { + this._refreshToken = refreshToken; + this._tokenEnd = new Date(); + this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() - 10); + } + + async getAccessToken(): Promise { + if (this._isTokenExpired()) { + await this._getRefreshToken(); + } + return this._accessToken; + } + + private async _getRefreshToken(): Promise { + + const response = await axios.get( + configs.API_URL + '/spotify/refresh?refresh_token=' + this._refreshToken + ) + if (response.status === 200) { + const responseData = await response.data; + this._accessToken = responseData.access_token; + this._tokenEnd = new Date(); + this._tokenEnd.setSeconds(this._tokenEnd.getSeconds() + responseData.expires_in) + } else { + console.log(`Error refreshing token: ${response.status}`) + } + } + + private _isTokenExpired(): boolean { + return new Date() > this._tokenEnd; + } + +} \ No newline at end of file diff --git a/src/FLAD/services/spotify/IspotifyAuthHandler.ts b/src/FLAD/services/spotify/IspotifyAuthHandler.ts deleted file mode 100644 index 47c5813..0000000 --- a/src/FLAD/services/spotify/IspotifyAuthHandler.ts +++ /dev/null @@ -1,4 +0,0 @@ -interface IspotifyAuthHandler { - abstract async getUserToken(): Promise; - abstract async getUserTokenFromRefreshToken(): Promise; -} \ No newline at end of file diff --git a/src/FLAD/services/spotify/spotify.service.ts b/src/FLAD/services/spotify/spotify.service.ts deleted file mode 100644 index 11e1b24..0000000 --- a/src/FLAD/services/spotify/spotify.service.ts +++ /dev/null @@ -1,156 +0,0 @@ -import axios from "axios"; -import MusicMapper from "../../model/mapper/MusicMapper"; -import Music from "../../model/Music"; -import { FetchOptions, RequestHandler } from "./spotifyRequestHandler/utils"; - -export class MusicMinimal { - public id: string; - public title: string; - public image: string; - - constructor(id: string, title: string, bio: string, image: string, trackPreviewUrl: string) { - this.title = title; - this.image = image; - this.id = id; - } -} - -export default class SpotifyService implements IspotifyService { - private readonly API_URL = API_URL; - private spotifyRequestHandler = new RequestHandler(); - public token: string; - - - constructor(token: string) { - this.token = token; - } - public async getMusicById(idMusic: string): Promise { - let requestData: string = '/tracks/' + idMusic; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - if (respMusic.status != 200) { - return null; - } - console.log(respMusic.data.artists[0].id); - - return MusicFactory.mapFromSpotifyTrack(respMusic.data); - } - - public async getUserCurrentMusic(): Promise { - let requestData: string = '/me/player/currently-playing'; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - if (respMusic.status != 200) { - return null; - } - - - return respMusic.data.item.id; - } - - public async getUserRecentlyPlayedMusic(): Promise { - let requestData: string = '/me/player/recently-played'; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - if (respMusic.status != 200) { - return null; - } - if (respMusic.data.items.length <= 0) { - return null; - } - return respMusic.data.items[0].track.id; - } - - public async playMusic(idMusic: string): Promise { - const fetchOptions: FetchOptions = { - method: 'PUT', - body: { - uris: [`spotify:track:${idMusic}`], - position_ms: 0 - } - }; - throw new Error("not Implemented") - } - - public async searchMusic(text: string): Promise { - const requestData: string = '/search'; - const fetchOptions: FetchOptions = { - params: { - q: text, - type: 'track' - } - }; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token); - - const tracksData = respMusic?.data?.tracks?.items; - if (!tracksData || !Array.isArray(tracksData)) { - return []; - } - const tracks = tracksData.map((trackData: any) => { - return MusicFactory.mapFromSpotifyTrack(trackData) - }); - return tracks; - } - // tempo version - public async getMusicMoreDetails(idMusic: string): Promise { - let requestData: string = '/audio-features/' + idMusic; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - return respMusic.data.audio_features.tempo; - } - - public async getRelatedArtist(idArtist: string): Promise { - let requestData: string = '/artists/' + idArtist + '/related-artists'; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - return respMusic.data.audio_features.tempo; - } - - public async getArtistTopTracks(idArtist: string): Promise { - let requestData: string = '/artists/' + idArtist + '/top-tracks'; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, undefined, this.token); - return respMusic.data.audio_features.tempo; - } - - public async addItemToPlayList(playlistId: string, idMusic: string): Promise { - let requestData: string = '/playlists/' + playlistId + '/tracks'; - const fetchOptions: FetchOptions = { - method: 'POST', - body: { - uris: [`spotify:track:${idMusic}`] - } - }; - const respMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, fetchOptions, this.token); - console.log(respMusic.data); - } - - public async getSimilarTrack(musicId: string, limit: number = 1, market?: string): Promise { - const requestData: string = '/recommendations/' + - '?limit=' + limit + - '&market=FR' + - '&seed_tracks=' + musicId; - let respSimilarMusic; - try { - respSimilarMusic = await this.spotifyRequestHandler.spotifyFetch(requestData, {}, this.token); - } catch (error) { - console.log(error); - } - if (!respSimilarMusic || !respSimilarMusic.data.tracks) { - return []; - } - const similars: Music[] = await Promise.all( - respSimilarMusic.data.tracks.map(async (trackData: any) => { - if (trackData.id != undefined) { - const data = await this.getMusicById(trackData.id); - return data; - } - - }) - - ) - return similars.filter((music: Music | undefined) => !!music) as Music[]; - } - - - async getSpotifyCredentials() { - const res = await axios.get(this.API_URL) - const spotifyCredentials = res.data; - return spotifyCredentials - } - -} diff --git a/src/FLAD/services/spotify/spotifyRequestHandler/IspotifyService.ts b/src/FLAD/services/spotify/spotifyRequestHandler/IspotifyService.ts deleted file mode 100644 index 560091c..0000000 --- a/src/FLAD/services/spotify/spotifyRequestHandler/IspotifyService.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface IspotifyService { - getMusicById(idMusic: string): Promise; -} \ No newline at end of file diff --git a/src/FLAD/services/spotify/spotifyRequestHandler/utils.tsx b/src/FLAD/services/spotify/spotifyRequestHandler/utils.tsx deleted file mode 100644 index 1e533a9..0000000 --- a/src/FLAD/services/spotify/spotifyRequestHandler/utils.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import axios, { AxiosResponse } from "axios"; -import { MY_SECURE_AUTH_STATE_KEY, MY_SECURE_AUTH_STATE_KEY_REFRESH } from "../../../screens/RegisterScreen"; -import * as SecureStore from 'expo-secure-store'; - -export type Methods = 'GET' | 'POST' | 'DELETE' | 'PUT' | 'PATCH'; - -export interface FetchOptions { - /** The headers that will be applied to the request when sent. */ - headers?: Record; - /** The type of HTTP method being used. */ - method?: Methods; - /** Parameters used for search queries.*/ - params?: Record; - /**If present, this refers to the JSON data that will be included in the request body. */ - body?: Record; -} - -export class RequestHandler { - private _version: `v${number}` = 'v1'; - - get version(): string { - return this._version; - } - public async spotifyFetch(url: string, options: FetchOptions = {}, token: string): Promise> { - try { - const resp = await axios({ - url: `https://api.spotify.com/${this.version}${url}`, - method: options.method || 'GET', - params: options.params, - headers: { - Authorization: "Bearer " + token, - Accept: 'application/json', - ...options.headers - }, - data: options.body - }); - - return resp; - } - catch(error : any){ - const errorMessage = error.response.data?.error?.message; - if (errorMessage === "Invalid access token" || errorMessage === "The access token expired" ) { - console.log('### Warning ! ### try refresh token Request Handler ' +error); - - const newToken = await this.refreshToken(); - console.log('### GOOD Warning ! ### new token Request Handler ' +newToken); - // Mettez à jour le token dans le store ou le reducer ici - return this.spotifyFetch(url, options, newToken); - } - else { - console.log('### Error ! ### while fetching Data in the SPotify Request Handler ' +error); - throw error; - } - } - } - private async refreshToken(): Promise { - - // Faites une demande à votre API pour obtenir un nouveau token Spotify - let refreshToken = await SecureStore.getItemAsync(MY_SECURE_AUTH_STATE_KEY_REFRESH); - console.log('refresh token : ' + refreshToken); - const response = await axios.get(`https://flad-api-production.up.railway.app/api/spotify/refresh?refresh_token=${refreshToken}`); - // Renvoie le nouveau token - const { - access_token : access_token, - refresh_token: refresh_token, - } = response.data as SpotifyAuthResponse - console.log('new access token : ' + access_token); - console.log('new refresh token : ' + refresh_token); - await SecureStore.setItemAsync(MY_SECURE_AUTH_STATE_KEY, access_token); - return access_token; - } -} - - -interface SpotifyAuthResponse { - access_token: string; - refresh_token: string; - } \ No newline at end of file diff --git a/src/FLAD/utils/MqttClient.js b/src/FLAD/utils/MqttClient.js deleted file mode 100644 index 59c3d25..0000000 --- a/src/FLAD/utils/MqttClient.js +++ /dev/null @@ -1,80 +0,0 @@ -import initialize from '../lib'; - -initialize(); -class MqttClient { - - constructor() { - const clientId = 'ReactNativeMqtt'; - this.client = new Paho.MQTT.Client('127.0.0.1', 9001, clientId); - this.client.onMessageArrived = this.onMessageArrived; - this.callbacks = {}; - this.onSuccessHandler = undefined; - this.onConnectionLostHandler = undefined; - this.isConnected = false; - } - - onConnect = (onSuccessHandler, onConnectionLostHandler) => { - this.onSuccessHandler = onSuccessHandler; - this.onConnectionLostHandler = onConnectionLostHandler; - this.client.onConnectionLost = () => { - this.isConnected = false; - onConnectionLostHandler(); - }; - - this.client.connect({ - timeout: 10, - onSuccess: () => { - this.isConnected = true; - onSuccessHandler(); - }, - useSSL: false, - onFailure: this.onError, - reconnect: true, - keepAliveInterval: 20, - cleanSession: true, - }); - }; - - onError = ({errorMessage}) => { - console.log(errorMessage); - this.isConnected = false; - Alert.alert('Failed', 'Failed to connect to MQTT', [ - { - text: 'Cancel', - onPress: () => console.log('Cancel Pressed'), - style: 'cancel', - }, - { - text: 'Try Again', - onPress: () => - this.onConnect( - this.onSuccessHandler, - this.onConnectionLostHandler, - ), - }, - ]); - }; - - onMessageArrived = message => { - const {payloadString, topic} = message; - console.log('onMessageArrived:', payloadString); - this.callbacks[topic](payloadString); - }; - - onPublish = (topic, message) => { - this.client.publish(topic, message); - }; - - onSubscribe = (topic, callback) => { - this.callbacks[topic] = callback; - this.client.subscribe(topic); - }; - - unsubscribe = topic => { - delete this.callbacks[topic]; - this.client.unsubscribe(topic); - }; -} - -let client = new MqttClient(); -export {client as MqttClient}; \ No newline at end of file