From 3175f77b7f11c13ba2a380a504c520547b157e7b Mon Sep 17 00:00:00 2001 From: Nuwan Date: Thu, 29 Aug 2024 17:08:19 +0530 Subject: [PATCH] implement profile photo upload --- .../components/navbar/JKProfileDropdown.js | 72 +++++++------- jam-ui/src/components/page/JKEditProfile.js | 50 ++++------ .../profile/JKProfileAvatarUpload.js | 97 +++++++++++-------- jam-ui/src/context/AppDataContext.js | 21 ++++ jam-ui/src/context/UserAuth.js | 23 ++--- jam-ui/src/helpers/rest.js | 18 +++- jam-ui/src/hooks/useUserProfile.js | 44 +++++++++ jam-ui/src/layouts/JKDashboardLayout.js | 20 ++-- .../20240828002334_add_v2_photo_attributes.rb | 8 ++ ruby/lib/jam_ruby/models/user.rb | 29 +++++- web/app/controllers/api_users_controller.rb | 27 +++++- web/app/views/api_users/profile_show.rabl | 2 +- web/app/views/api_users/show.rabl | 2 +- web/config/routes.rb | 3 + 14 files changed, 279 insertions(+), 137 deletions(-) create mode 100644 jam-ui/src/context/AppDataContext.js create mode 100644 jam-ui/src/hooks/useUserProfile.js create mode 100644 ruby/db/migrate/20240828002334_add_v2_photo_attributes.rb diff --git a/jam-ui/src/components/navbar/JKProfileDropdown.js b/jam-ui/src/components/navbar/JKProfileDropdown.js index 4d78dec75..80f83f745 100644 --- a/jam-ui/src/components/navbar/JKProfileDropdown.js +++ b/jam-ui/src/components/navbar/JKProfileDropdown.js @@ -4,59 +4,65 @@ import { DropdownItem, DropdownMenu, DropdownToggle, Dropdown } from 'reactstrap import { useAuth } from '../../context/UserAuth'; import JKProfileAvatar from '../profile/JKProfileAvatar'; import { useCookies } from 'react-cookie'; -import { useHistory } from "react-router-dom"; +import { useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +// import useUserProfile from '../../hooks/useUserProfile'; +import { useAppData } from '../../context/AppDataContext'; const ProfileDropdown = () => { const { t } = useTranslation(); const [dropdownOpen, setDropdownOpen] = useState(false); const toggle = () => setDropdownOpen(prevState => !prevState); - const { isAuthenticated, currentUser, setCurrentUser, logout } = useAuth(); + const { isAuthenticated, currentUser, setCurrentUser, logout, currentUserProfile } = useAuth(); const [cookies, setCookie, removeCookie] = useCookies(['remember_token']); const history = useHistory(); - const handleLogout = async (event) => { + // const { photoUrl } = useUserProfile(currentUser); + const { appData } = useAppData(); + const { currentUserPhotoUrl } = appData; + + const handleLogout = async event => { event.preventDefault(); removeCookie('remember_token', { domain: `.${process.env.REACT_APP_ORIGIN}` }); setCurrentUser(null); - await logout() - history.push('/') + await logout(); + history.push('/'); }; return ( <> - {isAuthenticated && - { - // let windowWidth = window.innerWidth; - // windowWidth > 992 && setDropdownOpen(true); - // }} - // onMouseLeave={() => { - // let windowWidth = window.innerWidth; - // windowWidth > 992 && setDropdownOpen(false); - // }} - > - - - {/* {currentUser && currentUser.name} */} - - -
- {/* + {isAuthenticated && ( + { + // let windowWidth = window.innerWidth; + // windowWidth > 992 && setDropdownOpen(true); + // }} + // onMouseLeave={() => { + // let windowWidth = window.innerWidth; + // windowWidth > 992 && setDropdownOpen(false); + // }} + > + + {} + {/* {currentUser && currentUser.name} */} + + +
+ {/* My Profile */} - {t('signout', { ns: 'auth' })} -
-
-
- } + {t('signout', { ns: 'auth' })} +
+
+
+ )} ); }; diff --git a/jam-ui/src/components/page/JKEditProfile.js b/jam-ui/src/components/page/JKEditProfile.js index 889b077ae..caaf904fe 100644 --- a/jam-ui/src/components/page/JKEditProfile.js +++ b/jam-ui/src/components/page/JKEditProfile.js @@ -1,5 +1,5 @@ import React, { useRef, useEffect, useLayoutEffect, useState, useReducer } from 'react'; -import { Card, CardBody, Col, Row, CardHeader, Form, FormGroup, Label, Input, Button } from 'reactstrap'; +import { Card, CardBody, Col, Row, CardHeader, Form, FormGroup, Label, Input } from 'reactstrap'; import Select from 'react-select'; import FalconCardHeader from '../common/FalconCardHeader'; import { useTranslation } from 'react-i18next'; @@ -7,7 +7,6 @@ import JKProfileAvatar from '../profile/JKProfileAvatar'; import { useAuth } from '../../context/UserAuth'; import { useForm, Controller } from 'react-hook-form'; import { - getPersonById, getInstruments, getGenres, updateUser, @@ -18,6 +17,8 @@ import { import JKProfileAvatarUpload from '../profile/JKProfileAvatarUpload'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Prompt } from 'react-router'; +// import useUserProfile from '../../hooks/useUserProfile'; +import { useAppData } from '../../context/AppDataContext'; function JKEditProfile() { const { t } = useTranslation('profile'); @@ -25,7 +26,6 @@ function JKEditProfile() { const [musicInstruments, setMusicInstruments] = useState([]); const [genres, setGenres] = useState([]); const [instrumentsInitialLoadingDone, setInstrumentsInitialLoadingDone] = useState(false); - const [currentUserLoaded, setCurrentUserLoaded] = useState(false); const [genreInitialLoadingDone, setGenreInitialLoadingDone] = useState(false); const [countries, setCountries] = useState([]); const [regions, setRegions] = useState([]); @@ -33,6 +33,10 @@ function JKEditProfile() { const [showAvatarUpload, setShowAvatarUpload] = useState(false); const [updating, setUpdating] = useState(false); + // const { userProfile, photoUrl } = useUserProfile(currentUser); + const { appData } = useAppData(); + const { currentUserPhotoUrl, userProfile } = appData; + const [_, forceUpdate] = useReducer(x => x + 1, 0); const saveTimeoutRef = useRef(null); @@ -62,35 +66,18 @@ function JKEditProfile() { } }); - useLayoutEffect(() => { - if (currentUser && !currentUserLoaded) { - setCurrentUserLoaded(true); - fetchCurentUser().then(data => { - console.log('userData', data); - updateUserData(data); - fetchInstruments(); - fetchGenres(); - fetchCountries(); - }); - } - }, [currentUser]); + useEffect(() => { + if (!userProfile) return; + updateFormData(userProfile); + }, [userProfile]); - const fetchCurentUser = () => { - return new Promise((resolve, reject) => { - getPersonById(currentUser.id) - .then(response => { - if (response.ok) { - return response.json(); - } else { - reject('Error fetching user data'); - } - }) - .then(data => resolve(data)) - .catch(error => reject(error)); - }); - }; + useEffect(() => { + fetchInstruments(); + fetchGenres(); + fetchCountries(); + }, []); - const updateUserData = data => { + const updateFormData = data => { setValue('firstName', data.first_name); setValue('lastName', data.last_name); setValue('country', data.country ? data.country : ''); @@ -221,7 +208,6 @@ function JKEditProfile() { const onSubmit = data => console.log(data); const handleInstrumentSelect = (e, musicInstrument) => { - //alert(e.target.checked) if (e.target.checked) { const userInstruments = getValues('instruments'); const thisInstrument = userInstruments.find( @@ -481,7 +467,7 @@ function JKEditProfile() { >
- + { }
diff --git a/jam-ui/src/components/profile/JKProfileAvatarUpload.js b/jam-ui/src/components/profile/JKProfileAvatarUpload.js index 731dd3015..3925ba1e5 100644 --- a/jam-ui/src/components/profile/JKProfileAvatarUpload.js +++ b/jam-ui/src/components/profile/JKProfileAvatarUpload.js @@ -5,30 +5,19 @@ import { useTranslation } from 'react-i18next'; import JKModalDialog from '../common/JKModalDialog'; import JKProfileAvatar from './JKProfileAvatar'; import { useAuth } from '../../context/UserAuth'; -import { getUserDetail, updateAvatar } from '../../helpers/rest'; +import { deleteAvatar, updateAvatar } from '../../helpers/rest'; +import { toast } from 'react-toastify'; +//import useUserProfile from '../../hooks/useUserProfile'; +import { useAppData } from '../../context/AppDataContext'; +import { set } from 'react-hook-form'; const JKProfileAvatarUpload = ({ show, toggle }) => { const { t } = useTranslation('profile'); const { currentUser } = useAuth(); - const [userDetails, setUserDetails] = useState(null); - - useEffect(() => { - if (currentUser) { - console.log('DEBUG', currentUser.id); - getUserDetail({ id: currentUser.id }) - .then(response => { - if (response.ok) { - return response.json(); - } - }) - .then(data => { - setUserDetails(data); - }) - .catch(error => { - console.error(error); - }); - } - }, [currentUser]); + // const { photoUrl } = useUserProfile(currentUser); + const { appData, setAppData } = useAppData(); + const { currentUserPhotoUrl } = appData; + const [isProcessing, setIsProsessing] = useState(false); const openFilePicker = () => { const client = filestack.init(window.gon.fp_apikey); @@ -45,7 +34,13 @@ const JKProfileAvatarUpload = ({ show, toggle }) => { }, storeTo: { location: 's3', - path: `${window.gon.fp_upload_dir}/users/${currentUser.id}/` + path: `${window.gon.fp_upload_dir}/${currentUser.id}/` + }, + onFileUploadStarted: () => { + setIsProsessing(true); + }, + onFileUploadFailed: () => { + setIsProsessing(false); }, onUploadDone: res => { console.log('onUploadDone', res); @@ -54,46 +49,70 @@ const JKProfileAvatarUpload = ({ show, toggle }) => { if (res.filesUploaded.length > 0) { const opts = { id: currentUser.id, - original_fpfile: userDetails.original_fpfile ? userDetails.original_fpfile : null, - cropped_fpfile: res.filesUploaded[0].url, - cropped_large_fpfile: null, - cropped_selection: null, - } - if(res.filesUploaded[0].cropped){ - opts['crop_selection'] = { - x: res.filesUploaded[0].cropped.cropArea.position[0], - y: res.filesUploaded[0].cropped.cropArea.position[1], - w: res.filesUploaded[0].cropped.cropArea.size[0], - h: res.filesUploaded[0].cropped.cropArea.size[1], - }; + url: res.filesUploaded[0].url, + // cropped_selection: null, } + // if(res.filesUploaded[0].cropped){ + // opts['crop_selection'] = { + // x: res.filesUploaded[0].cropped.cropArea.position[0], + // y: res.filesUploaded[0].cropped.cropArea.position[1], + // w: res.filesUploaded[0].cropped.cropArea.size[0], + // h: res.filesUploaded[0].cropped.cropArea.size[1], + // }; + // } updateAvatar(opts).then(response => { if (response.ok) { return response.json(); } }).then(data => { - alert('Photo updated successfully'); - console.log('DEBUG', data); + console.log('photo upload success', data); + setAppData({ + ...appData, + currentUserPhotoUrl: data.v2_photo_url + }); + toast.success('Success! Your avatar has been updated.'); + setIsProsessing(false); }).catch(error => { console.error(error); + toast.error('An error encountered when updating avatar.'); + setIsProsessing(false); }); } } }; client.picker(options).open(); }; + + const deleteProfileAvatar = () => { + setIsProsessing(true); + deleteAvatar(currentUser.id).then(response => { + if (response.ok) { + setAppData({ + ...appData, + currentUserPhotoUrl: null + }); + toast.success('Success! Your avatar has been deleted.'); + } + }).catch(error => { + console.error(error); + toast.error('An error encountered when deleting avatar.'); + }).finally(() => { + setIsProsessing(false); + }); + } + return (
- + { }
- +
- -
diff --git a/jam-ui/src/context/AppDataContext.js b/jam-ui/src/context/AppDataContext.js new file mode 100644 index 000000000..9053ee156 --- /dev/null +++ b/jam-ui/src/context/AppDataContext.js @@ -0,0 +1,21 @@ +import React from 'react'; +import useUserProfile from '../hooks/useUserProfile'; +import { useAuth } from './UserAuth'; + +// AppDataContext.js +// this context is used to store app data that is shared across the app +const AppDataContext = React.createContext(null); + +export const AppDataProvider = ({ children }) => { + const [appData, setAppData] = React.useState({}); + const { currentUser } = useAuth(); + const { userProfile, photoUrl } = useUserProfile(currentUser); + + React.useEffect(() => { + setAppData({ userProfile, currentUserPhotoUrl: photoUrl }); + }, [currentUser, userProfile, photoUrl]); + + return {children}; +}; + +export const useAppData = () => React.useContext(AppDataContext); \ No newline at end of file diff --git a/jam-ui/src/context/UserAuth.js b/jam-ui/src/context/UserAuth.js index 47e2b42cb..45653bf05 100644 --- a/jam-ui/src/context/UserAuth.js +++ b/jam-ui/src/context/UserAuth.js @@ -1,32 +1,31 @@ import React, { useState, useEffect, createContext, useContext } from 'react'; import PropTypes from 'prop-types'; -import { checkIsAuthenticated, authSignUp, authLogin, authLogout } from '../services/auth' +import { checkIsAuthenticated, authSignUp, authLogin, authLogout } from '../services/auth'; export const UserAuthContext = createContext({}); -export const useAuth = () => useContext(UserAuthContext) +export const useAuth = () => useContext(UserAuthContext); export default function UserAuth({ children, path }) { const [isAuthenticated, setIsAuthenticated] = useState(false); - const [currentUser, setCurrentUser] = useState(null) + const [currentUser, setCurrentUser] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { - //console.log('checking auth for', path); checkAuth(); }, [path]); const checkAuth = () => checkIsAuthenticated() .then(resp => resp.json()) - .then((user) => { + .then(user => { window.currentUser = user; - setCurrentUser(user) - setIsAuthenticated(true) + setCurrentUser(user); + setIsAuthenticated(true); }) .catch(() => { - setIsAuthenticated(false) - setCurrentUser(null) + setIsAuthenticated(false); + setCurrentUser(null); window.currentUser = null; }) .then(() => setIsLoading(false)); @@ -40,7 +39,7 @@ export default function UserAuth({ children, path }) { }); const logout = () => { - authLogout() + authLogout(); setIsAuthenticated(false); }; @@ -53,7 +52,9 @@ export default function UserAuth({ children, path }) { }); return ( - + {children} ); diff --git a/jam-ui/src/helpers/rest.js b/jam-ui/src/helpers/rest.js index 66f246104..986eb52ea 100644 --- a/jam-ui/src/helpers/rest.js +++ b/jam-ui/src/helpers/rest.js @@ -546,17 +546,25 @@ export const deleteMixdown = id => { export const updateAvatar = options => { const { id, ...rest } = options; const opts = { - original_fpfile: rest['original_fpfile'], - cropped_fpfile: rest['cropped_fpfile'], - cropped_large_fpfile: rest['cropped_large_fpfile'], - crop_selection: rest['crop_selection'] + url: rest['url'], + // crop_selection: rest['crop_selection'] }; return new Promise((resolve, reject) => { - apiFetch(`/users/${id}/avatar`, { + apiFetch(`/users/${id}/avatar_v2`, { method: 'POST', body: JSON.stringify(opts) }) .then(response => resolve(response)) .catch(error => reject(error)); }); +} + +export const deleteAvatar = id => { + return new Promise((resolve, reject) => { + apiFetch(`/users/${id}/avatar_v2`, { + method: 'DELETE' + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); } \ No newline at end of file diff --git a/jam-ui/src/hooks/useUserProfile.js b/jam-ui/src/hooks/useUserProfile.js new file mode 100644 index 000000000..6f3de8228 --- /dev/null +++ b/jam-ui/src/hooks/useUserProfile.js @@ -0,0 +1,44 @@ +import { getPersonById } from '../helpers/rest'; +import { useEffect, useState, useMemo } from 'react'; + +const useUserProfile = (user) => { + const [userProfile, setUserProfile] = useState(null) + + useEffect(() => { + if (!user) { + setUserProfile(null); + return; + } + getPersonById(user.id) + .then(response => { + if (response.ok) { + return response.json(); + } + }) + .then(data => { + setUserProfile(data) + }) + .catch(error => console.error(error)); + + return () => { + setUserProfile(null); + } + }, [user]); + + + const photoUrl = useMemo(() => { + if(userProfile && userProfile.v2_photo_uploaded){ + return userProfile.v2_photo_url + }else if(userProfile && !userProfile.v2_photo_uploaded){ + return user.photo_url + } + return null + }, [userProfile]) + + return{ + userProfile, + photoUrl + } +} + +export default useUserProfile; diff --git a/jam-ui/src/layouts/JKDashboardLayout.js b/jam-ui/src/layouts/JKDashboardLayout.js index 3df4cce0e..2edc2f920 100644 --- a/jam-ui/src/layouts/JKDashboardLayout.js +++ b/jam-ui/src/layouts/JKDashboardLayout.js @@ -4,12 +4,10 @@ import PropTypes from 'prop-types'; import DashboardMain from '../components/dashboard/JKDashboardMain'; import UserAuth from '../context/UserAuth'; import { BrowserQueryProvider } from '../context/BrowserQuery'; - import { NativeAppProvider } from '../context/NativeAppContext'; - import { JKLobbyChatProvider } from '../components/sessions/JKLobbyChatContext'; - import { AppRoutesProvider } from '../context/AppRoutesContext'; +import { AppDataProvider } from '../context/AppDataContext'; const DashboardLayout = ({ location }) => { useEffect(() => { @@ -19,13 +17,15 @@ const DashboardLayout = ({ location }) => { return ( - - - - - - - + + + + + + + + + ); diff --git a/ruby/db/migrate/20240828002334_add_v2_photo_attributes.rb b/ruby/db/migrate/20240828002334_add_v2_photo_attributes.rb new file mode 100644 index 000000000..47e66b899 --- /dev/null +++ b/ruby/db/migrate/20240828002334_add_v2_photo_attributes.rb @@ -0,0 +1,8 @@ + class AddV2PhotoAttributes < ActiveRecord::Migration + def self.up + execute("ALTER TABLE public.users ADD COLUMN v2_photo_url VARCHAR(2048); ALTER TABLE public.users ADD COLUMN v2_photo_uploaded BOOLEAN; UPDATE public.users SET v2_photo_uploaded = FALSE; ALTER TABLE public.users ALTER COLUMN v2_photo_uploaded SET DEFAULT FALSE;") + end + def self.down + execute("ALTER TABLE public.users DROP COLUMN v2_photo_url; ALTER TABLE public.users DROP COLUMN v2_photo_uploaded;") + end + end diff --git a/ruby/lib/jam_ruby/models/user.rb b/ruby/lib/jam_ruby/models/user.rb index d003f62d2..d82698601 100644 --- a/ruby/lib/jam_ruby/models/user.rb +++ b/ruby/lib/jam_ruby/models/user.rb @@ -73,7 +73,8 @@ module JamRuby after_save :update_teacher_pct - attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time + attr_accessible :first_name, :last_name, :email, :city, :password, :password_confirmation, :state, :country, :birth_date, :subscribe_email, :terms_of_service, :original_fpfile, :cropped_fpfile, :cropped_large_fpfile, :cropped_s3_path, :cropped_large_s3_path, :photo_url, :large_photo_url, :crop_selection, :used_current_month, :used_month_play_time, + :v2_photo_url, :v2_photo_uploaded # updating_password corresponds to a lost_password attr_accessor :test_drive_packaging, :validate_instruments, :updating_password, :updating_email, :updated_email, :update_email_confirmation_url, :administratively_created, :current_password, :setting_password, :confirm_current_password, :updating_avatar, :updating_progression_field, :mods_json, :expecting_gift_card, :purchase_required, :user_type @@ -295,7 +296,7 @@ module JamRuby validate :validate_musician_instruments, if: :validate_instruments validate :validate_current_password validate :validate_update_email - validate :validate_avatar_info + validate :validate_avatar_info, unless: :avatar_v2_available? validate :email_case_insensitive_uniqueness validate :validate_spammy_names validate :update_email_case_insensitive_uniqueness, :if => :updating_email @@ -671,6 +672,10 @@ module JamRuby end end + def avatar_v2_available? + self.v2_photo_url.present? + end + def email_case_insensitive_uniqueness # using the case insensitive unique check of active record will downcase the field, which is not what we want--we want to preserve original casing search = User.where("email ILIKE ?", self.email).first @@ -2054,6 +2059,13 @@ module JamRuby ) end + def update_avatar_v2(url) + self.update_attributes( + :v2_photo_url => url, + :v2_photo_uploaded => true + ) + end + def delete_avatar(aws_bucket) User.transaction do @@ -2078,6 +2090,19 @@ module JamRuby end + def delete_avatar_v2(aws_bucket) + User.transaction do + #TODO: delete the v2_photo_url from s3 + # unless self.v2_photo_url.nil? + # S3Util.delete(aws_bucket, self.v2_photo_url) + # end + + return self.update_attributes( + :v2_photo_url => nil + ) + end + end + # throws RecordNotFound if signup token is invalid; i.e., if it's nil, empty string, or not belonging to a user def self.signup_confirm(signup_token) if signup_token.nil? || signup_token.empty? diff --git a/web/app/controllers/api_users_controller.rb b/web/app/controllers/api_users_controller.rb index f2ac2d0bd..9c4931391 100644 --- a/web/app/controllers/api_users_controller.rb +++ b/web/app/controllers/api_users_controller.rb @@ -12,7 +12,7 @@ class ApiUsersController < ApiController :friend_show, :friend_destroy, # friends :notification_index, :notification_destroy, # notifications :band_invitation_index, :band_invitation_show, :band_invitation_update, # band invitations - :set_password, :begin_update_email, :update_avatar, :delete_avatar, :generate_filepicker_policy, :request_reset_password, + :set_password, :begin_update_email, :update_avatar, :update_avatar_v2, :delete_avatar, :delete_avatar_v2, :generate_filepicker_policy, :request_reset_password, :share_session, :share_recording, :affiliate_report, :audio_latency, :get_latencies, :broadcast_notification, :redeem_giftcard, :post_app_interactions] @@ -626,8 +626,6 @@ class ApiUsersController < ApiController cropped_large_fpfile = params[:cropped_large_fpfile] crop_selection = params[:crop_selection] - debugger - # public bucket to allow images to be available to public @user.update_avatar(original_fpfile, cropped_fpfile, cropped_large_fpfile, crop_selection, Rails.application.config.aws_bucket_public) @@ -638,6 +636,19 @@ class ApiUsersController < ApiController end end + def update_avatar_v2 + url = params[:url] + # crop_selection = params[:crop_selection] + + @user.update_avatar_v2(url) + + if @user.errors.any? + respond_with @user, status: :unprocessable_entity + else + respond_with @user, responder: ApiResponder, status: 200 + end + end + def delete_avatar @user.delete_avatar(Rails.application.config.aws_bucket_public) @@ -648,6 +659,16 @@ class ApiUsersController < ApiController end end + def delete_avatar_v2 + @user.delete_avatar_v2(Rails.application.config.aws_bucket_public) + + if @user.errors.any? + respond_with @user, status: :unprocessable_entity + else + respond_with @user, responder: ApiResponder, status: 204 + end + end + def generate_filepicker_policy # generates a soon-expiring filepicker policy so that a user can only upload to their own folder in their bucket diff --git a/web/app/views/api_users/profile_show.rabl b/web/app/views/api_users/profile_show.rabl index c8cc36d0d..0a1d169ed 100644 --- a/web/app/views/api_users/profile_show.rabl +++ b/web/app/views/api_users/profile_show.rabl @@ -1,7 +1,7 @@ object @profile attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :concert_count, :studio_session_count, :virtual_band, :virtual_band_commitment, :traditional_band, :traditional_band_commitment, :traditional_band_touring, :paid_sessions, :paid_sessions_hourly_rate, -:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp +:paid_sessions_daily_rate, :free_sessions, :cowriting, :cowriting_purpose, :subscribe_email, :is_a_teacher, :is_a_student, :last_active_timestamp, :v2_photo_url, :v2_photo_uploaded child :online_presences => :online_presences do attributes :id, :service_type, :username diff --git a/web/app/views/api_users/show.rabl b/web/app/views/api_users/show.rabl index bf7f2e4d3..fa96ae104 100644 --- a/web/app/views/api_users/show.rabl +++ b/web/app/views/api_users/show.rabl @@ -2,7 +2,7 @@ object @user attributes :id, :first_name, :last_name, :name, :city, :state, :country, :location, :online, :photo_url, :musician, :gender, :birth_date, :internet_service_provider, :friend_count, :liker_count, :like_count, :follower_count, :following_count, :admin, :recording_count, :session_count, :biography, :favorite_count, :audio_latency, :upcoming_session_count, :age, :website, :skill_level, :reuse_card, :email_needs_verification, :is_a_teacher, :is_a_student, :is_onboarder, :timezone, -:use_video_conferencing_server +:use_video_conferencing_server, :v2_photo_url, :v2_photo_uploaded node :location do |user| if user.musician? diff --git a/web/config/routes.rb b/web/config/routes.rb index e2a54add7..d8915371e 100644 --- a/web/config/routes.rb +++ b/web/config/routes.rb @@ -509,6 +509,9 @@ Rails.application.routes.draw do match '/users/:id/avatar' => 'api_users#delete_avatar', :via => :delete match '/users/:id/filepicker_policy' => 'api_users#generate_filepicker_policy', :via => :get + match '/users/:id/avatar_v2' => 'api_users#update_avatar_v2', :via => :post + match '/users/:id/avatar_v2' => 'api_users#delete_avatar_v2', :via => :delete + # user progression match '/users/progression/downloaded_client' => 'api_users#downloaded_client', :via => :post match '/users/progression/certified_gear' => 'api_users#qualified_gear', :via => :post