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