Browse Source

feat: activity feed for profile

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 5 years ago
parent
commit
570d9cae81

+ 1 - 0
backend/index.js

@@ -169,6 +169,7 @@ moduleManager.addModule("discord");
 moduleManager.addModule("io");
 moduleManager.addModule("logger");
 moduleManager.addModule("notifications");
+moduleManager.addModule("activities");
 moduleManager.addModule("playlists");
 moduleManager.addModule("punishments");
 moduleManager.addModule("songs");

+ 62 - 0
backend/logic/actions/activities.js

@@ -0,0 +1,62 @@
+'use strict';
+
+const async = require('async');
+
+const hooks = require('./hooks');
+const moduleManager = require("../../index");
+
+const db = moduleManager.modules["db"];
+const utils = moduleManager.modules["utils"];
+const logger = moduleManager.modules["logger"];
+const activities = moduleManager.modules["activities"];
+
+module.exports = {
+	/**
+	 * Gets a set of activities
+	 *
+	 * @param session
+	 * @param {String} userId - the user whose activities we are looking for
+	 * @param {Integer} set - the set number to return
+	 * @param cb
+	 */
+	getSet: (session, userId, set, cb) => {
+		async.waterfall([
+			next => {
+				db.models.activity.find({ userId, hidden: false }).skip(15 * (set - 1)).limit(15).sort("createdAt").exec(next);
+			},
+		], async (err, activities) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("ACTIVITIES_GET_SET", `Failed to get set ${set} from activities. "${err}"`);
+				return cb({ status: 'failure', message: err });
+			}
+
+			logger.success("ACTIVITIES_GET_SET", `Set ${set} from activities obtained successfully.`);
+			cb({ status: "success", data: activities });
+		});
+	},
+
+	/**
+	 * Hides an activity for a user
+	 * 
+	 * @param session
+	 * @param {String} activityId - the activity which should be hidden
+	 * @param cb
+	 */
+	hideActivity: hooks.loginRequired((session, activityId, cb) => {
+		async.waterfall([
+			next => {
+				db.models.activity.updateOne({ _id: activityId }, { $set: { hidden: true } }, next);
+			}
+		], async err => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("ACTIVITIES_HIDE_ACTIVITY", `Failed to hide activity ${activityId}. "${err}"`);
+				return cb({ status: "failure", message: err });
+			}
+			
+			logger.success("ACTIVITIES_HIDE_ACTIVITY", `Successfully hid activity ${activityId}.`);
+			cb({ status: "success" })
+		});
+	})
+};

+ 1 - 0
backend/logic/actions/index.js

@@ -7,6 +7,7 @@ module.exports = {
 	stations: require('./stations'),
 	playlists: require('./playlists'),
 	users: require('./users'),
+	activities: require('./activities'),
 	reports: require('./reports'),
 	news: require('./news'),
 	punishments: require('./punishments')

+ 45 - 5
backend/logic/actions/playlists.js

@@ -11,6 +11,7 @@ const utils = moduleManager.modules["utils"];
 const logger = moduleManager.modules["logger"];
 const playlists = moduleManager.modules["playlists"];
 const songs = moduleManager.modules["songs"];
+const activities = moduleManager.modules["activities"];
 
 cache.sub('playlist.create', playlistId => {
 	playlists.getPlaylist(playlistId, (err, playlist) => {
@@ -161,6 +162,7 @@ let lib = {
 				return cb({ status: 'failure', message: err});
 			}
 			cache.pub('playlist.create', playlist._id);
+			activities.addActivity(session.userId, "created_playlist", [ playlist._id ]);
 			logger.success("PLAYLIST_CREATE", `Successfully created private playlist for user "${session.userId}".`);
 			cb({ status: 'success', message: 'Successfully created playlist', data: {
 				_id: playlist._id
@@ -199,6 +201,34 @@ let lib = {
 		});
 	}),
 
+	/**
+	 * Obtains basic metadata of a playlist in order to format an activity
+	 *
+	 * @param session
+	 * @param playlistId - the playlist id
+	 * @param cb
+	 */
+	getPlaylistForActivity: (session, playlistId, cb) => {
+		async.waterfall([
+
+			(next) => {
+				playlists.getPlaylist(playlistId, next);
+			}
+
+		], async (err, playlist) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY", `Failed to obtain metadata of playlist ${playlistId} for activity formatting. "${err}"`);
+				return cb({ status: 'failure', message: err });
+			} else {
+				logger.success("PLAYLISTS_GET_PLAYLIST_FOR_ACTIVITY", `Obtained metadata of playlist ${playlistId} for activity formatting successfully.`);
+				cb({ status: "success", data: {
+					title: playlist.displayName
+				} });
+			}
+		});
+	},
+
 	//TODO Remove this
 	/**
 	 * Updates a private playlist
@@ -277,11 +307,12 @@ let lib = {
 	 * Adds a song to a private playlist
 	 *
 	 * @param {Object} session - the session object automatically added by socket.io
+	 * @param {Boolean} isSet - is the song part of a set of songs to be added
 	 * @param {String} songId - the id of the song we are trying to add
 	 * @param {String} playlistId - the id of the playlist we are adding the song to
 	 * @param {Function} cb - gets called with the result
 	 */
-	addSongToPlaylist: hooks.loginRequired((session, songId, playlistId, cb) => {
+	addSongToPlaylist: hooks.loginRequired((session, isSet, songId, playlistId, cb) => {
 		async.waterfall([
 			(next) => {
 				playlists.getPlaylist(playlistId, (err, playlist) => {
@@ -325,6 +356,7 @@ let lib = {
 				return cb({ status: 'failure', message: err});
 			} else {
 				logger.success("PLAYLIST_ADD_SONG", `Successfully added song "${songId}" to private playlist "${playlistId}" for user "${session.userId}".`);
+				if (!isSet) activities.addActivity(session.userId, "added_song_to_playlist", [ { songId, playlistId } ]);
 				cache.pub('playlist.addSong', { playlistId: playlist._id, song: newSong, userId: session.userId });
 				return cb({ status: 'success', message: 'Song has been successfully added to the playlist', data: playlist.songs });
 			}
@@ -345,6 +377,9 @@ let lib = {
 		let songsInPlaylistTotal = 0;
 		let songsSuccess = 0;
 		let songsFail = 0;
+
+		let addedSongs = [];
+
 		async.waterfall([
 			(next) => {
 				utils.getPlaylistFromYouTube(url, musicOnly, (songIds, otherSongIds) => {
@@ -363,14 +398,17 @@ let lib = {
 					if (processed === songIds.length) next();
 				}
 				for (let s = 0; s < songIds.length; s++) {
-					lib.addSongToPlaylist(session, songIds[s], playlistId, (res) => {
+					lib.addSongToPlaylist(session, true, songIds[s], playlistId, res => {
 						processed++;
-						if (res.status === "success") songsSuccess++;
-						else songsFail++;
+						if (res.status === "success") {
+							addedSongs.push(songIds[s]);
+							songsSuccess++;
+						} else songsFail++;
 						checkDone();
 					});
 				}
 			},
+			
 			(next) => {
 				playlists.getPlaylist(playlistId, next);
 			},
@@ -385,6 +423,7 @@ let lib = {
 				logger.error("PLAYLIST_IMPORT", `Importing a YouTube playlist to private playlist "${playlistId}" failed for user "${session.userId}". "${err}"`);
 				return cb({ status: 'failure', message: err});
 			} else {
+				activities.addActivity(session.userId, "added_songs_to_playlist", addedSongs);
 				logger.success("PLAYLIST_IMPORT", `Successfully imported a YouTube playlist to private playlist "${playlistId}" for user "${session.userId}". Videos in playlist: ${videosInPlaylistTotal}, songs in playlist: ${songsInPlaylistTotal}, songs successfully added: ${songsSuccess}, songs failed: ${songsFail}.`);
 				cb({
 					status: 'success',
@@ -602,7 +641,8 @@ let lib = {
 				return cb({ status: 'failure', message: err});
 			}
 			logger.success("PLAYLIST_REMOVE", `Successfully removed private playlist "${playlistId}" for user "${session.userId}".`);
-			cache.pub('playlist.delete', {userId: session.userId, playlistId});
+			cache.pub('playlist.delete', { userId: session.userId, playlistId });
+			activities.addActivity(session.userId, "deleted_playlist", [ playlistId ]);
 			return cb({ status: 'success', message: 'Playlist successfully removed' });
 		});
 	})

+ 36 - 2
backend/logic/actions/songs.js

@@ -12,6 +12,7 @@ const songs = moduleManager.modules["songs"];
 const cache = moduleManager.modules["cache"];
 const utils = moduleManager.modules["utils"];
 const logger = moduleManager.modules["logger"];
+const activities = moduleManager.modules["activities"];
 
 cache.sub('song.removed', songId => {
 	utils.emitToRoom('admin.songs', 'event:admin.song.removed', songId);
@@ -124,7 +125,7 @@ module.exports = {
 
 		async.waterfall([
 			(next) => {
-				db.models.song.findOne({ songId }).exec(next);
+				song.getSong(songId, next);
 			}
 		], async (err, song) => {
 			if (err) {
@@ -138,6 +139,38 @@ module.exports = {
 		});
 	}),
 
+	/**
+	 * Obtains basic metadata of a song in order to format an activity
+	 *
+	 * @param session
+	 * @param songId - the song id
+	 * @param cb
+	 */
+	getSongForActivity: (session, songId, cb) => {
+		async.waterfall([
+			(next) => {
+				songs.getSongFromId(songId, next);
+			}
+		], async (err, song) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("SONGS_GET_SONG_FOR_ACTIVITY", `Failed to obtain metadata of song ${songId} for activity formatting. "${err}"`);
+				return cb({ status: 'failure', message: err });
+			} else {
+				if (song) {
+					logger.success("SONGS_GET_SONG_FOR_ACTIVITY", `Obtained metadata of song ${songId} for activity formatting successfully.`);
+					cb({ status: "success", data: {
+						title: song.title,
+						thumbnail: song.thumbnail
+					} });
+				} else {
+					logger.error("SONGS_GET_SONG_FOR_ACTIVITY", `Song ${songId} does not exist so failed to obtain for activity formatting.`);
+					cb({ status: "failure" });
+				}
+			}
+		});
+	},
+
 	/**
 	 * Updates a song
 	 *
@@ -265,7 +298,7 @@ module.exports = {
 			songId = song._id;
 			db.models.user.findOne({ _id: session.userId }, (err, user) => {
 				if (user.liked.indexOf(songId) !== -1) return cb({ status: 'failure', message: 'You have already liked this song.' });
-				db.models.user.updateOne({_id: session.userId}, {$push: {liked: songId}, $pull: {disliked: songId}}, err => {
+				db.models.user.updateOne({ _id: session.userId }, { $push: { liked: songId }, $pull: { disliked: songId } }, err => {
 					if (!err) {
 						db.models.user.countDocuments({"liked": songId}, (err, likes) => {
 							if (err) return cb({ status: 'failure', message: 'Something went wrong while liking this song.' });
@@ -275,6 +308,7 @@ module.exports = {
 									if (err) return cb({ status: 'failure', message: 'Something went wrong while liking this song.' });
 									songs.updateSong(songId, (err, song) => {});
 									cache.pub('song.like', JSON.stringify({ songId: oldSongId, userId: session.userId, likes: likes, dislikes: dislikes }));
+									activities.addActivity(session.userId, "liked_song", [ songId ]);
 									return cb({ status: 'success', message: 'You have successfully liked this song.' });
 								});
 							});

+ 30 - 0
backend/logic/actions/stations.js

@@ -16,6 +16,7 @@ const utils = moduleManager.modules["utils"];
 const logger = moduleManager.modules["logger"];
 const stations = moduleManager.modules["stations"];
 const songs = moduleManager.modules["songs"];
+const activities = moduleManager.modules["activities"];
 
 let userList = {};
 let usersPerStation = {};
@@ -243,6 +244,33 @@ module.exports = {
 		});
 	},
 
+	/**
+	 * Obtains basic metadata of a station in order to format an activity
+	 *
+	 * @param session
+	 * @param stationId - the station id
+	 * @param cb
+	 */
+	getStationForActivity: (session, stationId, cb) => {
+		async.waterfall([
+			(next) => {
+				stations.getStation(stationId, next);
+			}
+		], async (err, station) => {
+			if (err) {
+				err = await utils.getError(err);
+				logger.error("STATIONS_GET_STATION_FOR_ACTIVITY", `Failed to obtain metadata of station ${stationId} for activity formatting. "${err}"`);
+				return cb({ status: 'failure', message: err });
+			} else {
+				logger.success("STATIONS_GET_STATION_FOR_ACTIVITY", `Obtained metadata of station ${stationId} for activity formatting successfully.`);
+				return cb({ status: "success", data: {
+					title: station.displayName,
+					thumbnail: station.currentSong ? station.currentSong.thumbnail : ""
+				} });
+			}
+		});
+	},
+
 	/**
 	 * Verifies that a station exists
 	 *
@@ -854,6 +882,7 @@ module.exports = {
 			}
 			logger.success("STATIONS_REMOVE", `Removing station "${stationId}" successfully.`);
 			cache.pub('station.remove', stationId);
+			activities.addActivity(session.userId, "deleted_station", [ stationId ]);
 			return cb({ 'status': 'success', 'message': 'Successfully removed.' });
 		});
 	}),
@@ -920,6 +949,7 @@ module.exports = {
 			}
 			logger.success("STATIONS_CREATE", `Created station "${station._id}" successfully.`);
 			cache.pub('station.create', station._id);
+			activities.addActivity(session.userId, "created_station", [ station._id ]);
 			return cb({'status': 'success', 'message': 'Successfully created station.'});
 		});
 	}),

+ 63 - 0
backend/logic/activities.js

@@ -0,0 +1,63 @@
+'use strict';
+
+const coreClass = require("../core");
+
+const async = require('async');
+const mongoose = require('mongoose');
+
+module.exports = class extends coreClass {
+	constructor(name, moduleManager) {
+		super(name, moduleManager);
+
+		this.dependsOn = ["db", "utils"];
+	}
+
+	initialize() {
+		return new Promise((resolve, reject) => {
+			this.setStage(1);
+
+			this.db = this.moduleManager.modules["db"];
+			this.io = this.moduleManager.modules["io"];
+			this.utils = this.moduleManager.modules["utils"];
+
+			resolve();
+		});
+	}
+
+	/**
+	 * 
+	 * @param {String} userId - the id of the user 
+	 * @param {String} activityType - what type of activity the user has completed e.g. liked a song
+	 * @param {Array} payload - what the activity was specifically related to e.g. the liked song(s)
+	 */
+	async addActivity(userId, activityType, payload) {
+		try { await this._validateHook(); } catch { return; }
+
+		async.waterfall([
+
+			next => {
+				const activity = new this.db.models.activity({
+					userId,
+					activityType,
+					payload
+				});
+
+				activity.save((err, activity) => {
+					if (err) return next(err);
+					next(null, activity);
+				});
+			},
+
+			(activity, next) => {
+				this.utils.socketsFromUser(activity.userId, sockets => {
+					sockets.forEach(socket => {
+						socket.emit('event:activity.create', activity);
+					});
+				});
+			}
+
+		], (err, activity) => {
+			// cb(err, activity);
+		});
+	}
+}

+ 3 - 2
backend/logic/app.js

@@ -133,8 +133,9 @@ module.exports = class extends coreClass {
 								}
 							], next);
 						}
+						
 						if (!body.id) return next("Something went wrong, no id.");
-						db.models.user.findOne({'services.github.id': body.id}, (err, user) => {
+						db.models.user.findOne({ 'services.github.id': body.id }, (err, user) => {
 							next(err, user, body);
 						});
 					},
@@ -167,7 +168,7 @@ module.exports = class extends coreClass {
 							if (email.primary) address = email.email.toLowerCase();
 						});
 
-						db.models.user.findOne({'email.address': address}, next);
+						db.models.user.findOne({ 'email.address': address }, next);
 					},
 
 					

+ 2 - 0
backend/logic/db/index.js

@@ -43,6 +43,7 @@ module.exports = class extends coreClass {
 						queueSong: new mongoose.Schema(require(`./schemas/queueSong`)),
 						station: new mongoose.Schema(require(`./schemas/station`)),
 						user: new mongoose.Schema(require(`./schemas/user`)),
+						activity: new mongoose.Schema(require(`./schemas/activity`)),
 						playlist: new mongoose.Schema(require(`./schemas/playlist`)),
 						news: new mongoose.Schema(require(`./schemas/news`)),
 						report: new mongoose.Schema(require(`./schemas/report`)),
@@ -54,6 +55,7 @@ module.exports = class extends coreClass {
 						queueSong: mongoose.model('queueSong', this.schemas.queueSong),
 						station: mongoose.model('station', this.schemas.station),
 						user: mongoose.model('user', this.schemas.user),
+						activity: mongoose.model('activity', this.schemas.activity),
 						playlist: mongoose.model('playlist', this.schemas.playlist),
 						news: mongoose.model('news', this.schemas.news),
 						report: mongoose.model('report', this.schemas.report),

+ 15 - 0
backend/logic/db/schemas/activity.js

@@ -0,0 +1,15 @@
+module.exports = {
+	createdAt: { type: Date, default: Date.now, required: true },
+	hidden: { type: Boolean, default: false, required: true },
+	userId: { type: String, required: true },
+	activityType: { type: String, enum: [
+		"created_station",
+		"deleted_station",
+		"created_playlist",
+		"deleted_playlist",
+		"liked_song",
+		"added_song_to_playlist",
+		"added_songs_to_playlist"
+	], required: true },
+	payload: { type: Array, required: true }
+}

+ 0 - 3
backend/logic/songs.js

@@ -5,9 +5,6 @@ const coreClass = require("../core");
 const async = require('async');
 const mongoose = require('mongoose');
 
-
-
-
 module.exports = class extends coreClass {
 	constructor(name, moduleManager) {
 		super(name, moduleManager);

+ 1 - 0
frontend/components/Modals/AddSongToPlaylist.vue

@@ -78,6 +78,7 @@ export default {
 		addSongToPlaylist(playlistId) {
 			this.socket.emit(
 				"playlists.addSongToPlaylist",
+				false,
 				this.currentSong.songId,
 				playlistId,
 				res => {

+ 1 - 0
frontend/components/Modals/Playlists/Edit.vue

@@ -271,6 +271,7 @@ export default {
 		addSongToPlaylist(id) {
 			this.socket.emit(
 				"playlists.addSongToPlaylist",
+				false,
 				id,
 				this.playlist._id,
 				res => {

+ 227 - 34
frontend/components/User/Show.vue

@@ -79,31 +79,44 @@
 				class="content recent-activity-tab"
 				v-if="activeTab === 'recentActivity'"
 			>
-				<div class="item activity">
-					<div class="thumbnail">
-						<img
-							src="https://cdn8.openculture.com/2018/02/26214611/Arlo-safe-e1519715317729.jpg"
-							alt=""
-						/>
-						<i class="material-icons activity-type-icon"
-							>playlist_add</i
-						>
-					</div>
-					<div class="left-part">
-						<p class="top-text">
-							Added
-							<strong>3 songs</strong>
-							to the playlist
-							<strong>Blues</strong>
-						</p>
-						<p class="bottom-text">3 hours ago</p>
-					</div>
-					<div class="actions">
-						<a class="hide-icon" href="#" @click="hideActivity()">
-							<i class="material-icons">visibility_off</i>
-						</a>
+				<div v-if="activities.length > 0">
+					<div
+						class="item activity"
+						v-for="activity in sortedActivities"
+						:key="activity._id"
+					>
+						<div class="thumbnail">
+							<img :src="activity.thumbnail" alt="" />
+							<i class="material-icons activity-type-icon">{{
+								activity.icon
+							}}</i>
+						</div>
+						<div class="left-part">
+							<p class="top-text" v-html="activity.message"></p>
+							<p class="bottom-text">
+								{{
+									formatDistance(
+										parseISO(activity.createdAt),
+										new Date(),
+										{ addSuffix: true }
+									)
+								}}
+							</p>
+						</div>
+						<div class="actions">
+							<a
+								class="hide-icon"
+								href="#"
+								@click="hideActivity(activity._id)"
+							>
+								<i class="material-icons">visibility_off</i>
+							</a>
+						</div>
 					</div>
 				</div>
+				<div v-else>
+					<h2>No recent activity.</h2>
+				</div>
 			</div>
 			<div class="content playlists-tab" v-if="activeTab === 'playlists'">
 				<div
@@ -147,7 +160,8 @@
 
 <script>
 import { mapState, mapActions } from "vuex";
-import { format, parseISO } from "date-fns";
+import { format, formatDistance, parseISO } from "date-fns";
+import Toast from "toasters";
 
 import MainHeader from "../MainHeader.vue";
 import MainFooter from "../MainFooter.vue";
@@ -168,16 +182,25 @@ export default {
 			notes: "",
 			isUser: false,
 			activeTab: "recentActivity",
-			playlists: []
+			playlists: [],
+			activities: []
 		};
 	},
-	computed: mapState({
-		role: state => state.user.auth.role,
-		userId: state => state.user.auth.userId,
-		...mapState("modals", {
-			modals: state => state.modals.station
-		})
-	}),
+	computed: {
+		...mapState({
+			role: state => state.user.auth.role,
+			userId: state => state.user.auth.userId,
+			...mapState("modals", {
+				modals: state => state.modals.station
+			})
+		}),
+		sortedActivities() {
+			const { activities } = this;
+			return activities.sort(
+				(x, y) => new Date(y.createdAt) - new Date(x.createdAt)
+			);
+		}
+	},
 	mounted() {
 		lofig.get("frontendDomain").then(frontendDomain => {
 			this.frontendDomain = frontendDomain;
@@ -204,12 +227,48 @@ export default {
 								if (res.status === "success")
 									this.playlists = res.data;
 							});
+
+							this.socket.emit(
+								"activities.getSet",
+								this.userId,
+								1,
+								res => {
+									if (res.status === "success") {
+										for (
+											let a = 0;
+											a < res.data.length;
+											a += 1
+										) {
+											this.formatActivity(
+												res.data[a],
+												activity => {
+													this.activities.unshift(
+														activity
+													);
+												}
+											);
+										}
+									}
+								}
+							);
+
+							this.socket.on(
+								"event:activity.create",
+								activity => {
+									console.log(activity);
+									this.formatActivity(activity, activity => {
+										this.activities.unshift(activity);
+									});
+								}
+							);
+
 							this.socket.on(
 								"event:playlist.create",
 								playlist => {
 									this.playlists.push(playlist);
 								}
 							);
+
 							this.socket.on(
 								"event:playlist.delete",
 								playlistId => {
@@ -222,6 +281,7 @@ export default {
 									);
 								}
 							);
+
 							this.socket.on("event:playlist.addSong", data => {
 								this.playlists.forEach((playlist, index) => {
 									if (playlist._id === data.playlistId) {
@@ -231,6 +291,7 @@ export default {
 									}
 								});
 							});
+
 							this.socket.on(
 								"event:playlist.removeSong",
 								data => {
@@ -261,6 +322,7 @@ export default {
 									);
 								}
 							);
+
 							this.socket.on(
 								"event:playlist.updateDisplayName",
 								data => {
@@ -285,6 +347,8 @@ export default {
 		});
 	},
 	methods: {
+		formatDistance,
+		parseISO,
 		switchTab(tabName) {
 			this.activeTab = tabName;
 		},
@@ -300,8 +364,132 @@ export default {
 			});
 			return this.utils.formatTimeLong(length);
 		},
-		hideActivity() {
-			console.log("hidden activity");
+		hideActivity(activityId) {
+			this.socket.emit("activities.hideActivity", activityId, res => {
+				if (res.status === "success") {
+					this.activities = this.activities.filter(
+						activity => activity._id !== activityId
+					);
+				} else {
+					new Toast({ content: res.message, timeout: 3000 });
+				}
+			});
+		},
+		formatActivity(res, cb) {
+			console.log("activity", res);
+
+			const icons = {
+				created_station: "radio",
+				deleted_station: "delete",
+				created_playlist: "playlist_add_check",
+				deleted_playlist: "delete_sweep",
+				liked_song: "favorite",
+				added_song_to_playlist: "playlist_add",
+				added_songs_to_playlist: "playlist_add"
+			};
+
+			const activity = {
+				...res,
+				thumbnail: "",
+				message: "",
+				icon: ""
+			};
+
+			const plural = activity.payload.length > 1;
+
+			activity.icon = icons[activity.activityType];
+
+			if (activity.activityType === "created_station") {
+				this.socket.emit(
+					"stations.getStationForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Created the station <strong>${res.data.title}</strong>`;
+							activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Created a station";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "deleted_station") {
+				activity.message = `Deleted a station`;
+				return cb(activity);
+			}
+			if (activity.activityType === "created_playlist") {
+				this.socket.emit(
+					"playlists.getPlaylistForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Created the playlist <strong>${res.data.title}</strong>`;
+							// activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Created a playlist";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "deleted_playlist") {
+				activity.message = `Deleted a playlist`;
+				return cb(activity);
+			}
+			if (activity.activityType === "liked_song") {
+				if (plural) {
+					activity.message = `Liked ${activity.payload.length} songs.`;
+					return cb(activity);
+				}
+				this.socket.emit(
+					"songs.getSongForActivity",
+					activity.payload[0],
+					res => {
+						if (res.status === "success") {
+							activity.message = `Liked the song <strong>${res.data.title}</strong>`;
+							activity.thumbnail = res.data.thumbnail;
+							return cb(activity);
+						}
+						activity.message = "Liked a song";
+						return cb(activity);
+					}
+				);
+			}
+			if (activity.activityType === "added_song_to_playlist") {
+				this.socket.emit(
+					"songs.getSongForActivity",
+					activity.payload[0].songId,
+					song => {
+						console.log(song);
+						this.socket.emit(
+							"playlists.getPlaylistForActivity",
+							activity.payload[0].playlistId,
+							playlist => {
+								if (song.status === "success") {
+									if (playlist.status === "success")
+										activity.message = `Added the song <strong>${song.data.title}</strong> to the playlist <strong>${playlist.data.title}</strong>`;
+									else
+										activity.message = `Added the song <strong>${song.data.title}</strong> to a playlist`;
+									activity.thumbnail = song.data.thumbnail;
+									return cb(activity);
+								}
+								if (playlist.status === "success") {
+									activity.message = `Added a song to the playlist <strong>${playlist.data.title}</strong>`;
+									return cb(activity);
+								}
+								activity.message = "Added a song to a playlist";
+								return cb(activity);
+							}
+						);
+					}
+				);
+			}
+			if (activity.activityType === "added_songs_to_playlist") {
+				activity.message = `Added ${activity.payload.length} songs to a playlist`;
+				return cb(activity);
+			}
+			return false;
 		},
 		...mapActions("modals", ["openModal"]),
 		...mapActions("user/playlists", ["editPlaylist"])
@@ -489,9 +677,14 @@ export default {
 				line-height: 19px;
 				margin-bottom: 0;
 				margin-top: 6px;
+
+				&:first-letter {
+					text-transform: uppercase;
+				}
 			}
 
 			.thumbnail {
+				position: relative;
 				display: flex;
 				align-items: center;
 				justify-content: center;