Browse Source

feat(Preferences): now stored in database instead of localstorage

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 4 years ago
parent
commit
c149ad303b

+ 119 - 0
backend/logic/actions/users.js

@@ -18,6 +18,17 @@ const PunishmentsModule = moduleManager.modules.punishments;
 const ActivitiesModule = moduleManager.modules.activities;
 const PlaylistsModule = moduleManager.modules.playlists;
 
+CacheModule.runJob("SUB", {
+	channel: "user.updatePreferences",
+	cb: res => {
+		IOModule.runJob("SOCKETS_FROM_USER", { userId: res.userId }).then(response => {
+			response.sockets.forEach(socket => {
+				socket.emit("keep.event:user.preferences.changed", res.preferences);
+			});
+		});
+	}
+});
+
 CacheModule.runJob("SUB", {
 	channel: "user.updateUsername",
 	cb: user => {
@@ -619,6 +630,13 @@ export default {
 		);
 	}),
 
+	/**
+	 * Updates the order of a user's playlists
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Array} orderOfPlaylists - array of playlist ids (with a specific order)
+	 * @param {Function} cb - gets called with the result
+	 */
 	updateOrderOfPlaylists: isLoginRequired(async function updateOrderOfPlaylists(session, orderOfPlaylists, cb) {
 		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
@@ -660,6 +678,107 @@ export default {
 		);
 	}),
 
+	/**
+	 * Updates a user's preferences
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {object} preferences - object containing preferences
+	 * @param {boolean} preferences.nightMode - whether or not the user is using the night mode theme
+	 * @param {boolean} preferences.autoSkipDisliked - whether to automatically skip disliked songs
+	 * @param {Function} cb - gets called with the result
+	 */
+	updatePreferences: isLoginRequired(async function updatePreferences(session, preferences, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+		async.waterfall(
+			[
+				next => {
+					userModel.updateOne(
+						{ _id: session.userId },
+						{ $set: { preferences } },
+						{ runValidators: true },
+						next
+					);
+				}
+			],
+			async err => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log(
+						"ERROR",
+						"UPDATE_USER_PREFERENCES",
+						`Couldn't update preferences for user "${session.userId}" to "${preferences}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				CacheModule.runJob("PUB", {
+					channel: "user.updatePreferences",
+					value: {
+						preferences,
+						userId: session.userId
+					}
+				});
+
+				this.log(
+					"SUCCESS",
+					"UPDATE_USER_PREFERENCES",
+					`Updated preferences for user "${session.userId}" to "${preferences}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Preferences successfully updated"
+				});
+			}
+		);
+	}),
+
+	/**
+	 * Retrieves a user's preferences
+	 *
+	 * @param {object} session - the session object automatically added by socket.io
+	 * @param {Function} cb - gets called with the result
+	 */
+	getPreferences: isLoginRequired(async function updatePreferences(session, cb) {
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+		async.waterfall(
+			[
+				next => {
+					userModel.findById(session.userId).select({ preferences: -1 }).exec(next);
+				}
+			],
+			async (err, { preferences }) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+
+					this.log(
+						"ERROR",
+						"GET_USER_PREFERENCES",
+						`Couldn't retrieve preferences for user "${session.userId}". "${err}"`
+					);
+
+					return cb({ status: "failure", message: err });
+				}
+
+				this.log(
+					"SUCCESS",
+					"GET_USER_PREFERENCES",
+					`Successfully obtained preferences for user "${session.userId}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Preferences successfully retrieved",
+					data: preferences
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Gets user object from username (only a few properties)
 	 *

+ 2 - 2
backend/logic/db/schemas/user.js

@@ -41,7 +41,7 @@ export default {
 	createdAt: { type: Date, default: Date.now },
 	preferences: {
 		orderOfPlaylists: [{ type: mongoose.Schema.Types.ObjectId }],
-		nightMode: { type: Boolean, default: false },
-		autoSkipDisabled: { type: Boolean, default: true }
+		nightmode: { type: Boolean, default: false, required: true },
+		autoSkipDisliked: { type: Boolean, default: true, required: true }
 	}
 };

+ 18 - 12
frontend/src/App.vue

@@ -76,15 +76,6 @@ export default {
 			else this.disableNightMode();
 		}
 	},
-	beforeMount() {
-		const nightmode =
-			false || JSON.parse(localStorage.getItem("nightmode"));
-
-		this.changeNightmode(nightmode);
-
-		if (nightmode) this.enableNightMode();
-		else this.disableNightMode();
-	},
 	mounted() {
 		document.onkeydown = ev => {
 			const event = ev || window.event;
@@ -153,9 +144,21 @@ export default {
 			}
 		});
 		io.getSocket(true, socket => {
-			socket.on("keep.event:user.session.removed", () => {
-				window.location.reload();
+			this.socket = socket;
+
+			this.socket.emit("users.getPreferences", res => {
+				if (res.status === "success") {
+					this.changeAutoSkipDisliked(res.data.autoSkipDisliked);
+
+					this.changeNightmode(res.data.nightmode);
+					if (this.nightmode) this.enableNightMode();
+					else this.disableNightMode();
+				}
 			});
+
+			this.socket.on("keep.event:user.session.removed", () =>
+				window.location.reload()
+			);
 		});
 	},
 	methods: {
@@ -173,7 +176,10 @@ export default {
 				.classList.remove("night-mode");
 		},
 		...mapActions("modals", ["closeCurrentModal"]),
-		...mapActions("user/preferences", ["changeNightmode"])
+		...mapActions("user/preferences", [
+			"changeNightmode",
+			"changeAutoSkipDisliked"
+		])
 	}
 };
 </script>

+ 19 - 5
frontend/src/main.js

@@ -148,12 +148,26 @@ lofig.get("serverDomain").then(serverDomain => {
 			});
 		});
 
-		socket.on("keep.event:banned", ban => {
-			store.dispatch("user/auth/banUser", ban);
-		});
+		socket.on("keep.event:banned", ban =>
+			store.dispatch("user/auth/banUser", ban)
+		);
+
+		socket.on("event:user.username.changed", username =>
+			store.dispatch("user/auth/updateUsername", username)
+		);
+
+		socket.on("keep.event:user.preferences.changed", preferences => {
+			console.log("changed");
 
-		socket.on("event:user.username.changed", username => {
-			store.dispatch("user/auth/updateUsername", username);
+			store.dispatch(
+				"user/preferences/changeAutoSkipDisliked",
+				preferences.autoSkipDisliked
+			);
+
+			store.dispatch(
+				"user/preferences/changeNightmode",
+				preferences.nightmode
+			);
 		});
 	});
 });

+ 72 - 19
frontend/src/pages/Settings/tabs/Preferences.vue

@@ -1,5 +1,13 @@
 <template>
 	<div class="content preferences-tab">
+		<h4 class="section-title">Change preferences</h4>
+
+		<p class="section-description">
+			Tailor these settings to your liking.
+		</p>
+
+		<hr class="section-horizontal-rule" />
+
 		<p class="control is-expanded checkbox-control">
 			<input type="checkbox" id="nightmode" v-model="localNightmode" />
 			<label for="nightmode">
@@ -18,16 +26,28 @@
 				<p>Automatically vote to skip disliked songs</p>
 			</label>
 		</p>
-		<button class="button is-primary save-changes" @click="saveChanges()">
-			Save changes
-		</button>
+		<transition name="saving-changes-transition" mode="out-in">
+			<button
+				class="button save-changes"
+				:class="saveButtonStyle"
+				@click="saveChanges()"
+				:key="saveStatus"
+				:disabled="saveStatus === 'disabled'"
+				v-html="saveButtonMessage"
+			/>
+		</transition>
 	</div>
 </template>
 
 <script>
 import { mapState, mapActions } from "vuex";
+import Toast from "toasters";
+
+import io from "../../../io";
+import SaveButton from "../mixins/SaveButton.vue";
 
 export default {
+	mixins: [SaveButton],
 	data() {
 		return {
 			localNightmode: false,
@@ -39,26 +59,59 @@ export default {
 		autoSkipDisliked: state => state.user.preferences.autoSkipDisliked
 	}),
 	mounted() {
-		this.localNightmode = this.nightmode;
-		this.localAutoSkipDisliked = this.autoSkipDisliked;
+		io.getSocket(socket => {
+			this.socket = socket;
+
+			this.socket.emit("users.getPreferences", res => {
+				if (res.status === "success") {
+					this.localNightmode = res.data.nightmode;
+					this.localAutoSkipDisliked = res.data.autoSkipDisliked;
+				}
+			});
+
+			socket.on("keep.event:user.preferences.changed", preferences => {
+				this.localNightmode = preferences.nightmode;
+				this.localAutoSkipDisliked = preferences.autoSkipDisliked;
+			});
+		});
 	},
 	methods: {
 		saveChanges() {
-			if (this.localNightmode !== this.nightmode)
-				this.changeNightmodeLocal();
-			if (this.localAutoSkipDisliked !== this.autoSkipDisliked)
-				this.changeAutoSkipDislikedLocal();
-		},
-		changeNightmodeLocal() {
-			localStorage.setItem("nightmode", this.localNightmode);
-			this.changeNightmode(this.localNightmode);
-		},
-		changeAutoSkipDislikedLocal() {
-			localStorage.setItem(
-				"autoSkipDisliked",
-				this.localAutoSkipDisliked
+			if (
+				this.localNightmode === this.nightmode &&
+				this.localAutoSkipDisliked === this.autoSkipDisliked
+			) {
+				new Toast({
+					content: "Please make a change before saving.",
+					timeout: 5000
+				});
+
+				return this.failedSave();
+			}
+
+			this.saveStatus = "disabled";
+
+			return this.socket.emit(
+				"users.updatePreferences",
+				{
+					nightmode: this.localNightmode,
+					autoSkipDisliked: this.localAutoSkipDisliked
+				},
+				res => {
+					if (res.status !== "success") {
+						new Toast({ content: res.message, timeout: 8000 });
+
+						return this.failedSave();
+					}
+
+					new Toast({
+						content: "Successfully updated preferences",
+						timeout: 4000
+					});
+
+					return this.successfulSave();
+				}
 			);
-			this.changeAutoSkipDisliked(this.localAutoSkipDisliked);
 		},
 		...mapActions("user/preferences", [
 			"changeNightmode",

+ 1 - 1
frontend/src/pages/Settings/tabs/Profile.vue

@@ -4,7 +4,7 @@
 			Change Profile
 		</h4>
 		<p class="section-description">
-			Edit your public profile so other users can find out more about you.
+			Edit your public profile so users can find out more about you.
 		</p>
 
 		<hr class="section-horizontal-rule" />

+ 1 - 8
frontend/src/pages/Station/index.vue

@@ -442,12 +442,6 @@ export default {
 			autoSkipDisliked: state => state.user.preferences.autoSkipDisliked
 		})
 	},
-	beforeMount() {
-		const autoSkipDisliked =
-			true || JSON.parse(localStorage.getItem("autoSkipDisliked"));
-
-		this.changeAutoSkipDisliked(autoSkipDisliked);
-	},
 	mounted() {
 		window.scrollTo(0, 0);
 
@@ -1507,8 +1501,7 @@ export default {
 			"updateNoSong",
 			"updateIfStationIsFavorited"
 		]),
-		...mapActions("editSongModal", ["stopVideo"]),
-		...mapActions("user/preferences", ["changeAutoSkipDisliked"])
+		...mapActions("editSongModal", ["stopVideo"])
 	}
 };
 </script>