Browse Source

feat: add preference for default station/playlist privacy, allow setting station privacy in create modal

Kristian Vos 1 month ago
parent
commit
ad62ddd6ab

+ 3 - 3
backend/logic/actions/stations.js

@@ -1810,7 +1810,7 @@ export default {
 				},
 
 				(playlist, stationId, next) => {
-					const { name, displayName, description, type } = data;
+					const { name, displayName, description, type, privacy } = data;
 					if (type === "official") {
 						stationModel.create(
 							{
@@ -1820,7 +1820,7 @@ export default {
 								description,
 								playlist: playlist._id,
 								type,
-								privacy: "private",
+								privacy,
 								queue: [],
 								currentSong: null
 							},
@@ -1835,7 +1835,7 @@ export default {
 								description,
 								playlist: playlist._id,
 								type,
-								privacy: "private",
+								privacy,
 								owner: session.userId,
 								queue: [],
 								currentSong: null

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

@@ -836,6 +836,8 @@ export default {
 	 * @param {boolean} preferences.activityLogPublic - whether or not a user's activity log can be publicly viewed
 	 * @param {boolean} preferences.anonymousSongRequests - whether or not a user's requested songs will be anonymous
 	 * @param {boolean} preferences.activityWatch - whether or not a user is using the ActivityWatch integration
+	 * @param {boolean} preferences.defaultStationPrivacy - default station privacy
+	 * @param {boolean} preferences.defaultPlaylistPrivacy - default playlist privacy
 	 * @param {Function} cb - gets called with the result
 	 */
 	updatePreferences: isLoginRequired(async function updatePreferences(session, preferences, cb) {

+ 1 - 1
backend/logic/db/index.js

@@ -14,7 +14,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	report: 7,
 	song: 10,
 	station: 10,
-	user: 4,
+	user: 5,
 	youtubeApiRequest: 1,
 	youtubeVideo: [1, 2],
 	youtubeChannel: 1,

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

@@ -42,7 +42,14 @@ export default {
 		autoSkipDisliked: { type: Boolean, default: true, required: true },
 		activityLogPublic: { type: Boolean, default: false, required: true },
 		anonymousSongRequests: { type: Boolean, default: false, required: true },
-		activityWatch: { type: Boolean, default: false, required: true }
+		activityWatch: { type: Boolean, default: false, required: true },
+		defaultStationPrivacy: {
+			type: String,
+			enum: ["public", "unlisted", "private"],
+			default: "private",
+			required: true
+		},
+		defaultPlaylistPrivacy: { type: String, enum: ["public", "private"], default: "public", required: true }
 	},
-	documentVersion: { type: Number, default: 4, required: true }
+	documentVersion: { type: Number, default: 5, required: true }
 };

+ 35 - 0
backend/logic/migration/migrations/migration26.js

@@ -0,0 +1,35 @@
+/**
+ * Migration 26
+ *
+ * Migration for setting new user preferences to default
+ * @param {object} MigrationModule - the MigrationModule
+ * @returns {Promise} - returns promise
+ */
+export default async function migrate(MigrationModule) {
+	const userModel = await MigrationModule.runJob("GET_MODEL", { modelName: "user" }, this);
+
+	return new Promise((resolve, reject) => {
+		this.log("INFO", `Migration 26. Updating users with document version 4.`);
+		userModel.updateMany(
+			{ documentVersion: 4 },
+			{
+				$set: {
+					documentVersion: 5,
+					"preferences.defaultStationPrivacy": "private",
+					"preferences.defaultPlaylistPrivacy": "public"
+				}
+			},
+			(err, res) => {
+				if (err) reject(new Error(err));
+				else {
+					this.log(
+						"INFO",
+						`Migration 26. Matched: ${res.matchedCount}, modified: ${res.modifiedCount}, ok: ${res.ok}.`
+					);
+
+					resolve();
+				}
+			}
+		);
+	});
+}

+ 17 - 1
frontend/src/App.vue

@@ -49,7 +49,9 @@ const {
 	changeAutoSkipDisliked,
 	changeActivityLogPublic,
 	changeAnonymousSongRequests,
-	changeActivityWatch
+	changeActivityWatch,
+	changeDefaultStationPrivacy,
+	changeDefaultPlaylistPrivacy
 } = userPreferencesStore;
 const { activeModals } = storeToRefs(modalsStore);
 const { openModal, closeCurrentModal } = modalsStore;
@@ -197,6 +199,12 @@ onMounted(async () => {
 						preferences.anonymousSongRequests
 					);
 					changeActivityWatch(preferences.activityWatch);
+					changeDefaultStationPrivacy(
+						preferences.defaultStationPrivacy
+					);
+					changeDefaultPlaylistPrivacy(
+						preferences.defaultPlaylistPrivacy
+					);
 				}
 			}
 		);
@@ -1644,6 +1652,14 @@ button.delete:focus {
 	}
 }
 
+.input-with-label {
+	column-gap: 8px;
+
+	.label {
+		align-items: center;
+	}
+}
+
 .page-title {
 	margin: 0 0 50px 0;
 }

+ 10 - 5
frontend/src/components/modals/CreatePlaylist.vue

@@ -1,9 +1,11 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref, onBeforeUnmount } from "vue";
 import Toast from "toasters";
+import { storeToRefs } from "pinia";
 import validation from "@/validation";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserPreferencesStore } from "@/stores/userPreferences";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
 
@@ -12,16 +14,19 @@ const props = defineProps({
 	admin: { type: Boolean, default: false }
 });
 
+const { openModal, closeCurrentModal } = useModalsStore();
+
+const { socket } = useWebsocketsStore();
+
+const userPreferencesStore = useUserPreferencesStore();
+const { defaultPlaylistPrivacy } = storeToRefs(userPreferencesStore);
+
 const playlist = ref({
 	displayName: "",
-	privacy: "public",
+	privacy: defaultPlaylistPrivacy.value,
 	songs: []
 });
 
-const { openModal, closeCurrentModal } = useModalsStore();
-
-const { socket } = useWebsocketsStore();
-
 const createPlaylist = () => {
 	const { displayName } = playlist.value;
 

+ 18 - 4
frontend/src/components/modals/CreateStation.vue

@@ -1,8 +1,10 @@
 <script setup lang="ts">
 import { defineAsyncComponent, ref } from "vue";
 import Toast from "toasters";
+import { storeToRefs } from "pinia";
 import { useWebsocketsStore } from "@/stores/websockets";
 import { useModalsStore } from "@/stores/modals";
+import { useUserPreferencesStore } from "@/stores/userPreferences";
 import validation from "@/validation";
 
 const Modal = defineAsyncComponent(() => import("@/components/Modal.vue"));
@@ -16,15 +18,19 @@ const { socket } = useWebsocketsStore();
 
 const { closeCurrentModal } = useModalsStore();
 
+const userPreferencesStore = useUserPreferencesStore();
+const { defaultStationPrivacy } = storeToRefs(userPreferencesStore);
+
 const newStation = ref({
 	name: "",
 	displayName: "",
-	description: ""
+	description: "",
+	privacy: defaultStationPrivacy.value
 });
 
 const submitModal = () => {
 	newStation.value.name = newStation.value.name.toLowerCase();
-	const { name, displayName, description } = newStation.value;
+	const { name, displayName, description, privacy } = newStation.value;
 
 	if (!name || !displayName || !description)
 		return new Toast("Please fill in all fields");
@@ -62,7 +68,8 @@ const submitModal = () => {
 			name,
 			type: props.official ? "official" : "community",
 			displayName,
-			description
+			description,
+			privacy
 		},
 		res => {
 			if (res.status === "success") {
@@ -107,9 +114,16 @@ const submitModal = () => {
 					class="input"
 					type="text"
 					placeholder="Description..."
-					@keyup.enter="submitModal()"
 				/>
 			</p>
+			<label class="label">Privacy</label>
+			<p class="control select">
+				<select v-model="newStation.privacy">
+					<option value="public">Public</option>
+					<option value="unlisted">Unlisted</option>
+					<option value="private">Private</option>
+				</select>
+			</p>
 		</template>
 		<template #footer>
 			<a class="button is-primary" @click="submitModal()">Create</a>

+ 9 - 1
frontend/src/main.ts

@@ -378,7 +378,9 @@ createSocket().then(async socket => {
 			changeNightmode,
 			changeActivityLogPublic,
 			changeAnonymousSongRequests,
-			changeActivityWatch
+			changeActivityWatch,
+			changeDefaultStationPrivacy,
+			changeDefaultPlaylistPrivacy
 		} = useUserPreferencesStore();
 
 		if (preferences.autoSkipDisliked !== undefined)
@@ -396,6 +398,12 @@ createSocket().then(async socket => {
 
 		if (preferences.activityWatch !== undefined)
 			changeActivityWatch(preferences.activityWatch);
+
+		if (preferences.defaultStationPrivacy !== undefined)
+			changeDefaultStationPrivacy(preferences.defaultStationPrivacy);
+
+		if (preferences.defaultPlaylistPrivacy !== undefined)
+			changeDefaultPlaylistPrivacy(preferences.defaultPlaylistPrivacy);
 	});
 
 	socket.on("keep.event:user.role.updated", res => {

+ 44 - 3
frontend/src/pages/Settings/Tabs/Preferences.vue

@@ -20,12 +20,17 @@ const localActivityLogPublic = ref(false);
 const localAnonymousSongRequests = ref(false);
 const localActivityWatch = ref(false);
 
+const localDefaultStationPrivacy = ref("private");
+const localDefaultPlaylistPrivacy = ref("public");
+
 const {
 	nightmode,
 	autoSkipDisliked,
 	activityLogPublic,
 	anonymousSongRequests,
-	activityWatch
+	activityWatch,
+	defaultStationPrivacy,
+	defaultPlaylistPrivacy
 } = storeToRefs(userPreferencesStore);
 
 const saveChanges = () => {
@@ -34,7 +39,9 @@ const saveChanges = () => {
 		localAutoSkipDisliked.value === autoSkipDisliked.value &&
 		localActivityLogPublic.value === activityLogPublic.value &&
 		localAnonymousSongRequests.value === anonymousSongRequests.value &&
-		localActivityWatch.value === activityWatch.value
+		localActivityWatch.value === activityWatch.value &&
+		localDefaultStationPrivacy.value === defaultStationPrivacy.value &&
+		localDefaultPlaylistPrivacy.value === defaultPlaylistPrivacy.value
 	) {
 		new Toast("Please make a change before saving.");
 
@@ -50,7 +57,9 @@ const saveChanges = () => {
 			autoSkipDisliked: localAutoSkipDisliked.value,
 			activityLogPublic: localActivityLogPublic.value,
 			anonymousSongRequests: localAnonymousSongRequests.value,
-			activityWatch: localActivityWatch.value
+			activityWatch: localActivityWatch.value,
+			defaultStationPrivacy: localDefaultStationPrivacy.value,
+			defaultPlaylistPrivacy: localDefaultPlaylistPrivacy.value
 		},
 		res => {
 			if (res.status !== "success") {
@@ -76,6 +85,10 @@ onMounted(() => {
 				localAnonymousSongRequests.value =
 					preferences.anonymousSongRequests;
 				localActivityWatch.value = preferences.activityWatch;
+				localDefaultStationPrivacy.value =
+					preferences.defaultStationPrivacy;
+				localDefaultPlaylistPrivacy.value =
+					preferences.defaultPlaylistPrivacy;
 			}
 		});
 	});
@@ -98,6 +111,14 @@ onMounted(() => {
 
 		if (preferences.activityWatch !== undefined)
 			localActivityWatch.value = preferences.activityWatch;
+
+		if (preferences.defaultStationPrivacy !== undefined)
+			localDefaultStationPrivacy.value =
+				preferences.defaultStationPrivacy;
+
+		if (preferences.defaultPlaylistPrivacy !== undefined)
+			localDefaultPlaylistPrivacy.value =
+				preferences.defaultPlaylistPrivacy;
 	});
 });
 </script>
@@ -187,6 +208,26 @@ onMounted(() => {
 			</label>
 		</p>
 
+		<div class="control is-grouped input-with-label">
+			<div class="control select">
+				<select v-model="localDefaultStationPrivacy">
+					<option value="public">Public</option>
+					<option value="unlisted">Unlisted</option>
+					<option value="private">Private</option>
+				</select>
+			</div>
+			<label class="label"> Default station privacy </label>
+		</div>
+
+		<div class="control is-grouped input-with-label">
+			<div class="control select">
+				<select v-model="localDefaultPlaylistPrivacy">
+					<option value="public">Public</option>
+					<option value="private">Private</option>
+				</select>
+			</div>
+			<label class="label"> Default playlist privacy </label>
+		</div>
 		<SaveButton ref="saveButton" @clicked="saveChanges()" />
 	</div>
 </template>

+ 7 - 1
frontend/src/pages/Settings/index.vue

@@ -193,13 +193,19 @@ onMounted(() => {
 		margin: 24px 0;
 		height: fit-content;
 
-		.control:not(:first-of-type) {
+		.control.checkbox-control:not(:first-of-type),
+		.control.input-with-label {
 			margin: 10px 0;
 		}
 
+		.select {
+			margin: 0 !important;
+		}
+
 		label {
 			font-size: 14px;
 			color: var(--dark-grey-2);
+			font-weight: 500;
 		}
 
 		textarea {

+ 11 - 1
frontend/src/stores/userPreferences.ts

@@ -7,12 +7,16 @@ export const useUserPreferencesStore = defineStore("userPreferences", {
 		activityLogPublic: boolean;
 		anonymousSongRequests: boolean;
 		activityWatch: boolean;
+		defaultStationPrivacy: "public" | "unlisted" | "private";
+		defaultPlaylistPrivacy: "public" | "private";
 	} => ({
 		nightmode: false,
 		autoSkipDisliked: true,
 		activityLogPublic: false,
 		anonymousSongRequests: false,
-		activityWatch: false
+		activityWatch: false,
+		defaultStationPrivacy: "private",
+		defaultPlaylistPrivacy: "public"
 	}),
 	actions: {
 		changeNightmode(nightmode) {
@@ -30,6 +34,12 @@ export const useUserPreferencesStore = defineStore("userPreferences", {
 		},
 		changeActivityWatch(activityWatch) {
 			this.activityWatch = activityWatch;
+		},
+		changeDefaultStationPrivacy(defaultStationPrivacy) {
+			this.defaultStationPrivacy = defaultStationPrivacy;
+		},
+		changeDefaultPlaylistPrivacy(defaultPlaylistPrivacy) {
+			this.defaultPlaylistPrivacy = defaultPlaylistPrivacy;
 		}
 	}
 });

+ 2 - 0
frontend/src/types/user.ts

@@ -48,5 +48,7 @@ export interface User {
 		activityLogPublic: boolean;
 		anonymousSongRequests: boolean;
 		activityWatch: boolean;
+		defaultStationPrivacy: "public" | "unlisted" | "private";
+		defaultPlaylistPrivacy: "public" | "private";
 	};
 }

+ 2 - 0
types/models/User.ts

@@ -5,6 +5,8 @@ export type UserPreferences = {
 	activityLogPublic: boolean;
 	anonymousSongRequests: boolean;
 	activityWatch: boolean;
+	defaultStationPrivacy: "public" | "unlisted" | "private";
+	defaultPlaylistPrivacy: "public" | "private";
 };
 
 export type UserModel = {