mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Remove localstorage and add notifications (#7588)
* move remaining files frm /common/script/public to website/public * remove localstorage * add back noscript template and put all javascript in the footer * fixes client side tests * remove double quotes where possible * simplify jade code and add tests for buildManifest * loading page with logo and spinner * better loading screen in landscape mode * icon on top of text logo * wip: user.notifications * notifications: simpler and working code * finish implementing notifications * correct loading screen css and re-inline images * add tests for user notifications * split User model in multiple files * remove old comment about missing .catch() * correctly setup hooks and methods for User model. Cleanup localstorage * include UserNotificationsService in static page js and split loading-screen css in its own file * add cron notification and misc fixes * remove console.log * fix tests * fix multiple notifications
This commit is contained in:
127
website/client/js/config.js
Normal file
127
website/client/js/config.js
Normal file
@@ -0,0 +1,127 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.config(['$httpProvider', function($httpProvider){
|
||||
$httpProvider.interceptors.push(['$q', '$rootScope', function($q, $rootScope){
|
||||
var resyncNumber = 0;
|
||||
var lastResync = 0;
|
||||
|
||||
// Verify that the user was not updated from another browser/app/client
|
||||
// If it was, sync
|
||||
function verifyUserUpdated (response) {
|
||||
var isApiCall = response.config.url.indexOf('api/v3') !== -1;
|
||||
var isUserAvailable = $rootScope.appLoaded === true;
|
||||
var hasUserV = response.data && response.data.userV;
|
||||
var isNotSync = response.config.url.indexOf('/api/v3/user') !== 0 || response.config.method !== 'GET';
|
||||
|
||||
if (isApiCall && isUserAvailable && hasUserV) {
|
||||
var oldUserV = $rootScope.User.user._v;
|
||||
$rootScope.User.user._v = response.data.userV;
|
||||
|
||||
// Something has changed on the user object that was not tracked here, sync the user
|
||||
if (isNotSync && ($rootScope.User.user._v - oldUserV) > 1) {
|
||||
$rootScope.User.sync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function verifyNewNotifications (response) {
|
||||
// Ignore CRON notifications for manual syncs
|
||||
var isUserLoaded = $rootScope.appLoaded === true;
|
||||
|
||||
if (response && response.data && response.data.notifications && response.data.notifications.length > 0) {
|
||||
$rootScope.userNotifications = response.data.notifications.filter(function (notification) {
|
||||
if (isUserLoaded && notification.type === 'CRON') {
|
||||
// If the user is already loaded, do not show the notification, syncing will show it
|
||||
// (the user will be synced automatically)
|
||||
$rootScope.User.readNotification(notification.id);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
request: function (config) {
|
||||
var url = config.url;
|
||||
|
||||
if (url.indexOf('api/v3') !== -1) {
|
||||
if ($rootScope.User && $rootScope.User.user) {
|
||||
if (url.indexOf('?') !== -1) {
|
||||
config.url += '&userV=' + $rootScope.User.user._v;
|
||||
} else {
|
||||
config.url += '?userV=' + $rootScope.User.user._v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
},
|
||||
response: function(response) {
|
||||
verifyUserUpdated(response);
|
||||
verifyNewNotifications(response);
|
||||
return response;
|
||||
},
|
||||
responseError: function(response) {
|
||||
var mobileApp = !!window.env.appVersion;
|
||||
|
||||
// Offline
|
||||
if (response.status == 0 ||
|
||||
// don't know why we're getting 404 here, should be 0
|
||||
(response.status == 404 && _.isEmpty(response.data))) {
|
||||
|
||||
if (!mobileApp) // skip mobile, queue actions
|
||||
$rootScope.$broadcast('responseText', window.env.t('serverUnreach'));
|
||||
|
||||
// Needs refresh
|
||||
} else if (response.needRefresh) {
|
||||
if (!mobileApp) // skip mobile for now
|
||||
$rootScope.$broadcast('responseError', "The site has been updated and the page needs to refresh. The last action has not been recorded, please refresh and try again.");
|
||||
|
||||
} else if (response.data && response.data.code && response.data.code === 'ACCOUNT_SUSPENDED') {
|
||||
confirm(response.data.err);
|
||||
localStorage.clear();
|
||||
window.location.href = mobileApp ? '/app/login' : '/logout'; //location.reload()
|
||||
|
||||
// 400 range
|
||||
} else if (response.status < 400) {
|
||||
// never triggered because we're in responseError
|
||||
$rootScope.$broadcast('responseText', response.data && response.data.message);
|
||||
} else if (response.status < 500) {
|
||||
if (response.status === 400 && response.data && response.data.errors && _.isArray(response.data.errors)) { // bad requests with more info
|
||||
response.data.errors.forEach(function (err) {
|
||||
$rootScope.$broadcast('responseError', err.message);
|
||||
});
|
||||
} else {
|
||||
$rootScope.$broadcast('responseError', response.data && response.data.message);
|
||||
}
|
||||
|
||||
if ($rootScope.User && $rootScope.User.sync) {
|
||||
if (resyncNumber < 100 && (Date.now() - lastResync) > 500) { // avoid thousands of requests when user is not found
|
||||
$rootScope.User.sync();
|
||||
resyncNumber++;
|
||||
lastResync = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
// Need to reject the prompse so the error is handled correctly
|
||||
if (response.status === 401) {
|
||||
return $q.reject(response);
|
||||
}
|
||||
// Error
|
||||
} else {
|
||||
var error = window.env.t('requestError') + '<br><br>"' +
|
||||
window.env.t('error') + ' ' + (response.data.message || response.data.error || response.data || 'something went wrong') +
|
||||
'" <br><br>' + window.env.t('seeConsole');
|
||||
if (mobileApp) error = 'Error contacting the server. Please try again in a few minutes.';
|
||||
$rootScope.$broadcast('responseError500', error);
|
||||
console.error(response);
|
||||
}
|
||||
|
||||
return $q.reject(response);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
}]);
|
||||
@@ -5,6 +5,7 @@ habitrpg
|
||||
function($scope, $rootScope, Members, Shared, $http, Notification, Groups, Chat, $controller, Stats) {
|
||||
|
||||
$controller('RootCtrl', {$scope: $scope});
|
||||
$rootScope.appLoaded = true;
|
||||
|
||||
$scope.timestamp = function(timestamp){
|
||||
return moment(timestamp).format($rootScope.User.user.preferences.dateFormat.toUpperCase());
|
||||
|
||||
@@ -23,17 +23,6 @@ habitrpg.controller('NotificationCtrl',
|
||||
Notification.exp(after - before);
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.achievements', function(){
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
}, true);
|
||||
|
||||
$rootScope.$watch('user.achievements.challenges.length', function(after, before) {
|
||||
if (after === before) return;
|
||||
if (after > before) {
|
||||
$rootScope.openModal('wonChallenge', {controller: 'UserCtrl', size: 'sm'});
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.stats.gp', function(after, before) {
|
||||
if (after == before) return;
|
||||
if (User.user.stats.lvl == 0) return;
|
||||
@@ -82,35 +71,89 @@ habitrpg.controller('NotificationCtrl',
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.achievements.streak', function(after, before){
|
||||
if(before == undefined || after <= before) return;
|
||||
Notification.streak(User.user.achievements.streak);
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
if (!User.user.preferences.suppressModals.streak) {
|
||||
$rootScope.openModal('achievements/streak', {controller:'UserCtrl'});
|
||||
}
|
||||
// Avoid showing the same notiication more than once
|
||||
var lastShownNotifications = [];
|
||||
|
||||
function handleUserNotifications (after) {
|
||||
if (!after || after.length === 0) return;
|
||||
|
||||
after.forEach(function (notification) {
|
||||
if (lastShownNotifications.indexOf(notification.id) !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastShownNotifications.push(notification.id);
|
||||
if (lastShownNotifications.length > 10) {
|
||||
lastShownNotifications.splice(0, 9);
|
||||
}
|
||||
|
||||
var markAsRead = true;
|
||||
|
||||
switch (notification.type) {
|
||||
case 'DROPS_ENABLED':
|
||||
$rootScope.openModal('dropsEnabled');
|
||||
break;
|
||||
case 'REBIRTH_ENABLED':
|
||||
$rootScope.openModal('rebirthEnabled');
|
||||
break;
|
||||
case 'WON_CHALLENGE':
|
||||
$rootScope.openModal('wonChallenge', {controller: 'UserCtrl', size: 'sm'});
|
||||
break;
|
||||
case 'STREAK_ACHIEVEMENT':
|
||||
Notification.streak(User.user.achievements.streak);
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
if (!User.user.preferences.suppressModals.streak) {
|
||||
$rootScope.openModal('achievements/streak', {controller:'UserCtrl'});
|
||||
}
|
||||
break;
|
||||
case 'ULTIMATE_GEAR_ACHIEVEMENT':
|
||||
$rootScope.openModal('achievements/ultimateGear', {controller:'UserCtrl'});
|
||||
break;
|
||||
case 'REBIRTH_ACHIEVEMENT':
|
||||
$rootScope.openModal('achievements/rebirth', {controller:'UserCtrl', size: 'sm'});
|
||||
break;
|
||||
case 'NEW_CONTRIBUTOR_LEVEL':
|
||||
$rootScope.openModal('achievements/contributor',{controller:'UserCtrl'});
|
||||
break;
|
||||
case 'CRON':
|
||||
if (notification.data) {
|
||||
if (notification.data.hp) Notification.hp(notification.data.hp, 'hp');
|
||||
if (notification.data.mp) Notification.mp(notification.data.mp);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
markAsRead = false; // If the notification is not implemented, skip it
|
||||
break;
|
||||
}
|
||||
|
||||
if (markAsRead) User.readNotification(notification.id);
|
||||
});
|
||||
|
||||
User.user.notifications = []; // reset the notifications
|
||||
}
|
||||
|
||||
// Since we don't use localStorage anymore, notifications for achievements and new contributor levels
|
||||
// are now stored user.notifications.
|
||||
$rootScope.$watchCollection('userNotifications', function (after) {
|
||||
if (!User.user._wrapped) return;
|
||||
handleUserNotifications(after);
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.achievements.ultimateGearSets', function(after, before){
|
||||
if (_.isEqual(after,before) || !_.contains(User.user.achievements.ultimateGearSets, true)) return;
|
||||
$rootScope.openModal('achievements/ultimateGear', {controller:'UserCtrl'});
|
||||
var handleUserNotificationsOnFirstSync = _.once(function () {
|
||||
handleUserNotifications($rootScope.userNotifications);
|
||||
});
|
||||
$rootScope.$on('userUpdated', handleUserNotificationsOnFirstSync);
|
||||
|
||||
// TODO what about this?
|
||||
$rootScope.$watch('user.achievements', function(){
|
||||
$rootScope.playSound('Achievement_Unlocked');
|
||||
}, true);
|
||||
|
||||
$rootScope.$watch('user.flags.armoireEmpty', function(after,before){
|
||||
if (before == undefined || after == before || after == false) return;
|
||||
if (after == before || after == false) return;
|
||||
$rootScope.openModal('armoireEmpty');
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.achievements.rebirths', function(after, before){
|
||||
if(after === before) return;
|
||||
$rootScope.openModal('achievements/rebirth', {controller:'UserCtrl', size: 'sm'});
|
||||
});
|
||||
|
||||
$rootScope.$watch('user.contributor.level', function(after, before){
|
||||
if (after === before || after < before || after == null) return;
|
||||
$rootScope.openModal('achievements/contributor',{controller:'UserCtrl'});
|
||||
});
|
||||
|
||||
// Completed quest modal
|
||||
$scope.$watch('user.party.quest.completed', function(after, before){
|
||||
if (!after) return;
|
||||
|
||||
@@ -25,6 +25,7 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
||||
}
|
||||
});
|
||||
|
||||
$rootScope.appLoaded = false; // also used to indicate when the user is fully loaded
|
||||
$rootScope.TAVERN_ID = TAVERN_ID;
|
||||
$rootScope.User = User;
|
||||
$rootScope.user = user;
|
||||
@@ -39,7 +40,8 @@ habitrpg.controller("RootCtrl", ['$scope', '$rootScope', '$location', 'User', '$
|
||||
$rootScope.Groups = Groups;
|
||||
$rootScope.toJson = angular.toJson;
|
||||
$rootScope.Payments = Payments;
|
||||
|
||||
$rootScope.userNotifications = [];
|
||||
|
||||
// Angular UI Router
|
||||
$rootScope.$state = $state;
|
||||
$rootScope.$stateParams = $stateParams;
|
||||
|
||||
78
website/client/js/directives/directives.js
Normal file
78
website/client/js/directives/directives.js
Normal file
@@ -0,0 +1,78 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Markdown
|
||||
*/
|
||||
(function(){
|
||||
var md = function () {
|
||||
var mdown = window.habiticaMarkdown;
|
||||
|
||||
var toHtml = function (markdown) {
|
||||
if (markdown == undefined)
|
||||
return '';
|
||||
|
||||
markdown = mdown.render(markdown);
|
||||
|
||||
return markdown;
|
||||
};
|
||||
|
||||
return {
|
||||
toHtml:toHtml
|
||||
};
|
||||
}();
|
||||
|
||||
habitrpg.directive('markdown', ['$timeout', function($timeout) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element, attrs) {
|
||||
var removeWatch = !!scope.$eval(attrs.removeWatch);
|
||||
var useTimeout = !!scope.$eval(attrs.useTimeout);
|
||||
var timeoutTime = scope.$eval(attrs.timeoutTime) || 0;
|
||||
|
||||
var doRemoveWatch = scope.$watch(attrs.text, function(value, oldValue) {
|
||||
var replaceMarkdown = function(){
|
||||
|
||||
var markdown = value;
|
||||
var linktarget = attrs.target || '_self';
|
||||
var userName = scope.User.user.profile.name;
|
||||
var userHighlight = "@"+userName;
|
||||
var html = md.toHtml(markdown);
|
||||
|
||||
html = html.replace(userHighlight, "<u>@"+userName+"</u>");
|
||||
|
||||
element.html(html);
|
||||
|
||||
if (removeWatch) {
|
||||
doRemoveWatch();
|
||||
}
|
||||
};
|
||||
|
||||
if(useTimeout) {
|
||||
$timeout(replaceMarkdown, timeoutTime);
|
||||
} else {
|
||||
replaceMarkdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
habitrpg.filter('markdown', function() {
|
||||
return function(input){
|
||||
var html = md.toHtml(input);
|
||||
|
||||
return html;
|
||||
};
|
||||
});
|
||||
})()
|
||||
|
||||
habitrpg.directive('questRewards', ['$rootScope', function($rootScope){
|
||||
return {
|
||||
restrict: 'AE',
|
||||
templateUrl: 'partials/options.social.party.quest-rewards.html',
|
||||
link: function(scope, element, attrs){
|
||||
scope.header = attrs.header || 'Rewards';
|
||||
scope.quest = $rootScope.Content.quests[attrs.key];
|
||||
}
|
||||
}
|
||||
}]);
|
||||
@@ -289,16 +289,6 @@ function($rootScope, User, $timeout, $state, Analytics) {
|
||||
case 'options.inventory.equipment': return goto('equipment', 0);
|
||||
}
|
||||
});
|
||||
$rootScope.$watch('user.flags.dropsEnabled', function(after, before) {
|
||||
if (alreadyShown(before,after)) return;
|
||||
var eggs = User.user.items.eggs || {};
|
||||
if (!eggs) eggs['Wolf'] = 1; // This is also set on the server
|
||||
$rootScope.openModal('dropsEnabled');
|
||||
});
|
||||
$rootScope.$watch('user.flags.rebirthEnabled', function(after, before) {
|
||||
if (alreadyShown(before, after)) return;
|
||||
$rootScope.openModal('rebirthEnabled');
|
||||
});
|
||||
});
|
||||
|
||||
var Guide = {
|
||||
|
||||
@@ -14,6 +14,7 @@ angular.module('habitrpg')
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: url,
|
||||
ignoreLoadingBar: $rootScope.appLoaded !== true,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
22
website/client/js/services/userNotificationsService.js
Normal file
22
website/client/js/services/userNotificationsService.js
Normal file
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
angular.module('habitrpg')
|
||||
.factory('UserNotifications', ['$http',
|
||||
function userNotificationsFactory($http) {
|
||||
|
||||
var lastRead; // keep track of last notification ID to avoid reding it twice
|
||||
|
||||
function readNotification (notificationId) {
|
||||
if (lastRead === notificationId) return;
|
||||
lastRead = notificationId;
|
||||
|
||||
return $http({
|
||||
method: 'POST',
|
||||
url: 'api/v3/notifications/' + notificationId + '/read',
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
readNotification: readNotification,
|
||||
};
|
||||
}]);
|
||||
@@ -14,8 +14,8 @@ angular.module('habitrpg')
|
||||
/**
|
||||
* Services that persists and retrieves user from localStorage.
|
||||
*/
|
||||
.factory('User', ['$rootScope', '$http', '$location', '$window', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'Notification', 'ApiUrl', 'Tasks', 'Tags', 'Content',
|
||||
function($rootScope, $http, $location, $window, STORAGE_USER_ID, STORAGE_SETTINGS_ID, Notification, ApiUrl, Tasks, Tags, Content) {
|
||||
.factory('User', ['$rootScope', '$http', '$location', '$window', 'STORAGE_USER_ID', 'STORAGE_SETTINGS_ID', 'Notification', 'ApiUrl', 'Tasks', 'Tags', 'Content', 'UserNotifications',
|
||||
function($rootScope, $http, $location, $window, STORAGE_USER_ID, STORAGE_SETTINGS_ID, Notification, ApiUrl, Tasks, Tags, Content, UserNotifications) {
|
||||
var authenticated = false;
|
||||
var defaultSettings = {
|
||||
auth: { apiId: '', apiToken: ''},
|
||||
@@ -38,11 +38,6 @@ angular.module('habitrpg')
|
||||
//first we populate user with schema
|
||||
user.apiToken = user._id = ''; // we use id / apitoken to determine if registered
|
||||
|
||||
//than we try to load localStorage
|
||||
if (localStorage.getItem(STORAGE_USER_ID)) {
|
||||
_.extend(user, JSON.parse(localStorage.getItem(STORAGE_USER_ID)));
|
||||
}
|
||||
|
||||
user._wrapped = false;
|
||||
|
||||
function syncUserTasks (tasks) {
|
||||
@@ -78,6 +73,7 @@ angular.module('habitrpg')
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: '/api/v3/user/',
|
||||
ignoreLoadingBar: $rootScope.appLoaded !== true,
|
||||
})
|
||||
.then(function (response) {
|
||||
if (response.data.message) Notification.text(response.data.message);
|
||||
@@ -108,14 +104,14 @@ angular.module('habitrpg')
|
||||
.then(function (response) {
|
||||
var tasks = response.data.data;
|
||||
syncUserTasks(tasks);
|
||||
save();
|
||||
$rootScope.$emit('userSynced');
|
||||
$rootScope.appLoaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
var save = function () {
|
||||
localStorage.setItem(STORAGE_USER_ID, JSON.stringify(user));
|
||||
localStorage.setItem(STORAGE_SETTINGS_ID, JSON.stringify(settings));
|
||||
localStorage.removeItem(STORAGE_USER_ID); // TODO remember to remove once it's been live for a few days
|
||||
};
|
||||
|
||||
function callOpsFunctionAndRequest (opName, endPoint, method, paramString, opData) {
|
||||
@@ -167,8 +163,6 @@ angular.module('habitrpg')
|
||||
var text = Content.gear.flat[openedItem.key].text();
|
||||
Notification.drop(env.t('messageDropMysteryItem', {dropText: text}), openedItem);
|
||||
}
|
||||
|
||||
save();
|
||||
})
|
||||
}
|
||||
|
||||
@@ -214,7 +208,6 @@ angular.module('habitrpg')
|
||||
} else {
|
||||
user.ops.addTask(data);
|
||||
}
|
||||
save();
|
||||
Tasks.createUserTasks(data.body);
|
||||
},
|
||||
|
||||
@@ -225,7 +218,6 @@ angular.module('habitrpg')
|
||||
Notification.text(err.message);
|
||||
return;
|
||||
}
|
||||
save();
|
||||
|
||||
Tasks.scoreTask(data.params.task._id, data.params.direction).then(function (res) {
|
||||
var tmp = res.data.data._tmp || {}; // used to notify drops, critical hits and other bonuses
|
||||
@@ -284,37 +276,35 @@ angular.module('habitrpg')
|
||||
|
||||
sortTask: function (data) {
|
||||
user.ops.sortTask(data);
|
||||
save();
|
||||
Tasks.moveTask(data.params.id, data.query.to);
|
||||
},
|
||||
|
||||
updateTask: function (task, data) {
|
||||
$window.habitrpgShared.ops.updateTask(task, data);
|
||||
save();
|
||||
Tasks.updateTask(task._id, data.body);
|
||||
},
|
||||
|
||||
deleteTask: function (data) {
|
||||
user.ops.deleteTask(data);
|
||||
save();
|
||||
Tasks.deleteTask(data.params.id);
|
||||
},
|
||||
|
||||
clearCompleted: function () {
|
||||
user.ops.clearCompleted(user.todos);
|
||||
save();
|
||||
Tasks.clearCompletedTodos();
|
||||
},
|
||||
|
||||
readNotification: function (notificationId) {
|
||||
UserNotifications.readNotification(notificationId);
|
||||
},
|
||||
|
||||
addTag: function(data) {
|
||||
user.ops.addTag(data);
|
||||
save();
|
||||
Tags.createTag(data.body);
|
||||
},
|
||||
|
||||
updateTag: function(data) {
|
||||
user.ops.updateTag(data);
|
||||
save();
|
||||
Tags.updateTag(data.params.id, data.body);
|
||||
},
|
||||
|
||||
@@ -326,7 +316,6 @@ angular.module('habitrpg')
|
||||
|
||||
deleteTag: function(data) {
|
||||
user.ops.deleteTag(data);
|
||||
save();
|
||||
Tags.deleteTag(data.params.id);
|
||||
},
|
||||
|
||||
@@ -495,7 +484,6 @@ angular.module('habitrpg')
|
||||
data: updates,
|
||||
})
|
||||
.then(function () {
|
||||
save();
|
||||
$rootScope.$emit('userSynced');
|
||||
})
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user