From 8219f1344865a712b3328f5059335b51edcd15a4 Mon Sep 17 00:00:00 2001
From: Marcelo Schmidt <marcelo.schmidt@gmail.com>
Date: Wed, 28 Dec 2016 18:04:38 -0200
Subject: [PATCH] Admin section for managing sounds

---
 .../admin/adminSounds.html                    |   6 +-
 .../admin/adminSounds.js                      |  19 ++-
 .../admin/soundEdit.html                      |   2 +-
 .../admin/soundEdit.js                        |  74 ++++++------
 .../admin/soundInfo.html                      |  19 +++
 .../admin/soundInfo.js                        | 111 ++++++++++++++++++
 .../assets/stylesheets/customSoundsAdmin.less | 102 ++++++++++++++++
 .../client/lib/CustomSounds.js                |  56 +++++++++
 .../client/lib/custom-sounds.js               |   3 -
 .../client/notifications/deleteCustomSound.js |   3 +
 .../client/notifications/updateCustomSound.js |   3 +
 packages/rocketchat-custom-sounds/package.js  |  16 ++-
 .../server/methods/deleteCustomSound.js       |  22 ++++
 .../server/methods/insertOrUpdateSound.js     |  62 ++++++++++
 .../server/methods/listCustomSounds.js        |   5 +
 .../server/methods/uploadCustomSound.js       |  20 ++++
 .../server/models/CustomSounds.js             |   9 ++
 .../server/publications/customSounds.js       |   3 +-
 .../server/startup/custom-sounds.js           |  38 +-----
 packages/rocketchat-i18n/i18n/en.i18n.json    |  17 ++-
 .../client/views/pushNotificationsFlexTab.js  |  19 ++-
 .../account/accountPreferences.coffee         |   2 +-
 .../rocketchat-ui/lib/notification.coffee     |  10 --
 packages/rocketchat-ui/package.js             |   1 -
 .../views/app/audioNotification.html          |  19 ++-
 .../views/app/audioNotification.js            |   6 -
 26 files changed, 517 insertions(+), 130 deletions(-)
 create mode 100644 packages/rocketchat-custom-sounds/admin/soundInfo.html
 create mode 100644 packages/rocketchat-custom-sounds/admin/soundInfo.js
 create mode 100644 packages/rocketchat-custom-sounds/assets/stylesheets/customSoundsAdmin.less
 create mode 100644 packages/rocketchat-custom-sounds/client/lib/CustomSounds.js
 delete mode 100644 packages/rocketchat-custom-sounds/client/lib/custom-sounds.js
 create mode 100644 packages/rocketchat-custom-sounds/client/notifications/deleteCustomSound.js
 create mode 100644 packages/rocketchat-custom-sounds/client/notifications/updateCustomSound.js
 create mode 100644 packages/rocketchat-custom-sounds/server/methods/deleteCustomSound.js
 create mode 100644 packages/rocketchat-custom-sounds/server/methods/insertOrUpdateSound.js
 create mode 100644 packages/rocketchat-custom-sounds/server/methods/listCustomSounds.js
 create mode 100644 packages/rocketchat-custom-sounds/server/methods/uploadCustomSound.js
 delete mode 100644 packages/rocketchat-ui/views/app/audioNotification.js

diff --git a/packages/rocketchat-custom-sounds/admin/adminSounds.html b/packages/rocketchat-custom-sounds/admin/adminSounds.html
index bcf0f377496..c891ff724e6 100644
--- a/packages/rocketchat-custom-sounds/admin/adminSounds.html
+++ b/packages/rocketchat-custom-sounds/admin/adminSounds.html
@@ -22,15 +22,13 @@
 					<table>
 						<thead>
 							<tr>
-								<th>{{_ "Name"}}</th>
-								<th>{{_ "Play"}}</th>
+								<th width="100%">{{_ "Name"}}</th>
 							</tr>
 						</thead>
 						<tbody>
 							{{#each customsounds}}
 							<tr class="sound-info row-link">
-								<td>{{name}}</td>
-								<td><i class="icon-play-circle"></i></td>
+								<td>{{name}}&nbsp;<i class="icon-play-circled"></i></td>
 							</tr>
 							{{/each}}
 						</tbody>
diff --git a/packages/rocketchat-custom-sounds/admin/adminSounds.js b/packages/rocketchat-custom-sounds/admin/adminSounds.js
index 67c6921e68b..32d7bb7a118 100644
--- a/packages/rocketchat-custom-sounds/admin/adminSounds.js
+++ b/packages/rocketchat-custom-sounds/admin/adminSounds.js
@@ -43,7 +43,7 @@ Template.adminSounds.onCreated(function() {
 		id: 'add-sound',
 		i18nTitle: 'Custom_Sound_Add',
 		icon: 'icon-plus',
-		template: 'adminSoundsEdit',
+		template: 'adminSoundEdit',
 		openClick(/*e, t*/) {
 			RocketChat.TabBar.setData();
 			return true;
@@ -56,7 +56,7 @@ Template.adminSounds.onCreated(function() {
 		id: 'admin-sound-info',
 		i18nTitle: 'Custom_Sound_Info',
 		icon: 'icon-cog',
-		template: 'adminSoundsInfo',
+		template: 'adminSoundInfo',
 		order: 2
 	});
 
@@ -73,7 +73,7 @@ Template.adminSounds.onCreated(function() {
 
 		if (filter) {
 			let filterReg = new RegExp(s.escapeRegExp(filter), 'i');
-			query = { $or: [ { name: filterReg }, {aliases: filterReg } ] };
+			query = { name: filterReg };
 		}
 
 		let limit = (isSetNotNull(() => instance.limit))? instance.limit.get() : 0;
@@ -106,8 +106,8 @@ Template.adminSounds.events({
 
 	['click .sound-info'](e) {
 		e.preventDefault();
-		RocketChat.TabBar.setTemplate('adminSoundsInfo');
-		RocketChat.TabBar.setData(RocketChat.models.CustomSound.findOne({_id: this._id}));
+		RocketChat.TabBar.setTemplate('adminSoundInfo');
+		RocketChat.TabBar.setData(RocketChat.models.CustomSounds.findOne({_id: this._id}));
 		RocketChat.TabBar.openFlex();
 		RocketChat.TabBar.showGroup('adminSounds-selected');
 	},
@@ -116,5 +116,14 @@ Template.adminSounds.events({
 		e.preventDefault();
 		e.stopPropagation();
 		t.limit.set(t.limit.get() + 50);
+	},
+
+	['click .icon-play-circled'](e) {
+		e.preventDefault();
+		e.stopPropagation();
+		const $audio = $('#' + this._id);
+		if ($audio && $audio[0] && $audio[0].play) {
+			$audio[0].play();
+		}
 	}
 });
diff --git a/packages/rocketchat-custom-sounds/admin/soundEdit.html b/packages/rocketchat-custom-sounds/admin/soundEdit.html
index b5e96922848..81b24b6fecc 100644
--- a/packages/rocketchat-custom-sounds/admin/soundEdit.html
+++ b/packages/rocketchat-custom-sounds/admin/soundEdit.html
@@ -12,7 +12,7 @@
 					<input type="text" id="name" autocomplete="off" value="{{sound.name}}">
 				</div>
 				<div class="input-line">
-					<label for="image">{{_ "Sound_File"}}</label>
+					<label for="image">{{_ "Sound_File_mp3"}}</label>
 					<input id="image" type="file" />
 				</div>
 				<nav>
diff --git a/packages/rocketchat-custom-sounds/admin/soundEdit.js b/packages/rocketchat-custom-sounds/admin/soundEdit.js
index 038f48b1d78..9a7ee93d5fa 100644
--- a/packages/rocketchat-custom-sounds/admin/soundEdit.js
+++ b/packages/rocketchat-custom-sounds/admin/soundEdit.js
@@ -83,7 +83,7 @@ Template.soundEdit.onCreated(function() {
 
 		if (!soundData._id) {
 			if (!this.soundFile) {
-				errors.push('Sound_File');
+				errors.push('Sound_File_mp3');
 			}
 		}
 
@@ -92,7 +92,7 @@ Template.soundEdit.onCreated(function() {
 		}
 
 		if (this.soundFile) {
-			if (!/audio\/mpeg/.test(this.soundFile.type)) {
+			if (!/audio\/mp3/.test(this.soundFile.type)) {
 				errors.push('FileType');
 				toastr.error(TAPi18n.__('error-invalid-file-type'));
 			}
@@ -108,44 +108,42 @@ Template.soundEdit.onCreated(function() {
 			if (this.soundFile) {
 				soundData.newFile = true;
 				soundData.extension = this.soundFile.name.split('.').pop();
+				soundData.type = this.soundFile.type;
 			}
 
-			console.log('save -> ', soundData);
-
-			// Meteor.call('insertOrUpdateSound', soundData, (error, result) => {
-			// 	if (result) {
-			// 		if (this.soundFile) {
-			// 			toastr.info(TAPi18n.__('Uploading_file'));
-
-			// 			let reader = new FileReader();
-			// 			reader.readAsBinaryString(this.soundFile);
-			// 			reader.onloadend = () => {
-			// 				Meteor.call('uploadSoundCustom', reader.result, this.soundFile.type, soundData, (uploadError/*, data*/) => {
-			// 					if (uploadError != null) {
-			// 						handleError(uploadError);
-			// 						console.log(uploadError);
-			// 						return;
-			// 					}
-			// 				}
-			// 				);
-			// 				delete this.soundFile;
-			// 				toastr.success(TAPi18n.__('File_uploaded'));
-			// 			};
-			// 		}
-
-			// 		if (soundData._id) {
-			// 			toastr.success(t('Custom_Sound_Updated_Successfully'));
-			// 		} else {
-			// 			toastr.success(t('Custom_Sound_Added_Successfully'));
-			// 		}
-
-			// 		this.cancel(form, soundData.name);
-			// 	}
-
-			// 	if (error) {
-			// 		handleError(error);
-			// 	}
-			// });
+			Meteor.call('insertOrUpdateSound', soundData, (error, result) => {
+				if (result) {
+					soundData._id = result;
+					soundData.random = Math.round(Math.random() * 1000);
+
+					if (this.soundFile) {
+						toastr.info(TAPi18n.__('Uploading_file'));
+
+						let reader = new FileReader();
+						reader.readAsBinaryString(this.soundFile);
+						reader.onloadend = () => {
+							Meteor.call('uploadCustomSound', reader.result, this.soundFile.type, soundData, (uploadError/*, data*/) => {
+								if (uploadError != null) {
+									handleError(uploadError);
+									console.log(uploadError);
+									return;
+								}
+							}
+							);
+							delete this.soundFile;
+							toastr.success(TAPi18n.__('File_uploaded'));
+						};
+					}
+
+					toastr.success(t('Custom_Sound_Saved_Successfully'));
+
+					this.cancel(form, soundData.name);
+				}
+
+				if (error) {
+					handleError(error);
+				}
+			});
 		}
 	};
 });
diff --git a/packages/rocketchat-custom-sounds/admin/soundInfo.html b/packages/rocketchat-custom-sounds/admin/soundInfo.html
new file mode 100644
index 00000000000..09374f00609
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/admin/soundInfo.html
@@ -0,0 +1,19 @@
+<template name="soundInfo">
+	{{#if editingSound}}
+		{{> soundEdit (soundToEdit)}}
+	{{else}}
+		{{#with sound}}
+		<div class="about clearfix">
+			<div class="info">
+				<h3 title="{{name}}">{{name}}</h3>
+			</div>
+		</div>
+		{{/with}}
+		<nav>
+			{{#if hasPermission 'manage-sounds'}}
+				<button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button>
+				<button class='button button-block primary edit-sound'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button>
+			{{/if}}
+		</nav>
+	{{/if}}
+</template>
diff --git a/packages/rocketchat-custom-sounds/admin/soundInfo.js b/packages/rocketchat-custom-sounds/admin/soundInfo.js
new file mode 100644
index 00000000000..f0f28e42892
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/admin/soundInfo.js
@@ -0,0 +1,111 @@
+/* globals isSetNotNull */
+Template.soundInfo.helpers({
+	name() {
+		let sound = Template.instance().sound.get();
+		return sound.name;
+	},
+
+	sound() {
+		return Template.instance().sound.get();
+	},
+
+	editingSound() {
+		return Template.instance().editingSound.get();
+	},
+
+	soundToEdit() {
+		let instance = Template.instance();
+		return {
+			sound: instance.sound.get(),
+			back(name) {
+				instance.editingSound.set();
+
+				if (isSetNotNull(() => name)) {
+					let sound = instance.sound.get();
+					if (isSetNotNull(() => sound.name) && sound.name !== name) {
+						return instance.loadedName.set(name);
+					}
+				}
+			}
+		};
+	}
+});
+
+Template.soundInfo.events({
+	['click .delete'](e, instance) {
+		e.stopPropagation();
+		e.preventDefault();
+		let sound = instance.sound.get();
+		if (isSetNotNull(() => sound)) {
+			let _id = sound._id;
+			swal({
+				title: t('Are_you_sure'),
+				text: t('Custom_Sound_Delete_Warning'),
+				type: 'warning',
+				showCancelButton: true,
+				confirmButtonColor: '#DD6B55',
+				confirmButtonText: t('Yes_delete_it'),
+				cancelButtonText: t('Cancel'),
+				closeOnConfirm: false,
+				html: false
+			}, function() {
+				swal.disableButtons();
+
+				Meteor.call('deleteCustomSound', _id, (error/*, result*/) => {
+					if (error) {
+						handleError(error);
+						swal.enableButtons();
+					} else {
+						swal({
+							title: t('Deleted'),
+							text: t('Custom_Sound_Has_Been_Deleted'),
+							type: 'success',
+							timer: 2000,
+							showConfirmButton: false
+						});
+
+						RocketChat.TabBar.showGroup('adminSounds');
+						RocketChat.TabBar.closeFlex();
+					}
+				});
+			});
+		}
+	},
+
+	['click .edit-sound'](e, instance) {
+		e.stopPropagation();
+		e.preventDefault();
+
+		instance.editingSound.set(instance.sound.get()._id);
+	}
+});
+
+Template.soundInfo.onCreated(function() {
+	this.sound = new ReactiveVar();
+
+	this.editingSound = new ReactiveVar();
+
+	this.loadedName = new ReactiveVar();
+
+	this.autorun(() => {
+		let data = Template.currentData();
+		if (isSetNotNull(() => data.clear)) {
+			this.clear = data.clear;
+		}
+	});
+
+	this.autorun(() => {
+		let data = Template.currentData();
+		let sound = this.sound.get();
+		if (isSetNotNull(() => sound.name)) {
+			this.loadedName.set(sound.name);
+		} else if (isSetNotNull(() => data.name)) {
+			this.loadedName.set(data.name);
+		}
+	});
+
+	this.autorun(() => {
+		let data = Template.currentData();
+		this.sound.set(data);
+	});
+});
diff --git a/packages/rocketchat-custom-sounds/assets/stylesheets/customSoundsAdmin.less b/packages/rocketchat-custom-sounds/assets/stylesheets/customSoundsAdmin.less
new file mode 100644
index 00000000000..4845388adc8
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/assets/stylesheets/customSoundsAdmin.less
@@ -0,0 +1,102 @@
+.sound-info {
+	.icon-play-circled {
+		cursor: pointer;
+	}
+}
+
+.sound-view {
+	z-index: 15;
+	overflow-y: auto;
+	overflow-x: hidden;
+
+	.thumb {
+		width: 100%;
+		height: 350px;
+		padding: 20px;
+	}
+
+	nav {
+		padding: 0 20px;
+	}
+
+	.info {
+		white-space: normal;
+		padding: 0 20px;
+
+		h3 {
+			-webkit-user-select: text;
+			-moz-user-select: text;
+			-ms-user-select: text;
+			user-select: text;
+			font-size: 24px;
+			margin: 8px 0;
+			line-height: 27px;
+			text-overflow: ellipsis;
+			width: 100%;
+			overflow: hidden;
+			white-space: nowrap;
+
+			i::after {
+				content: " ";
+				display: inline-block;
+				width: 8px;
+				height: 8px;
+				border-radius: 4px;
+				vertical-align: middle;
+			}
+		}
+
+		p {
+			-webkit-user-select: text;
+			-moz-user-select: text;
+			-ms-user-select: text;
+			user-select: text;
+			line-height: 18px;
+			font-size: 12px;
+			font-weight: 300;
+		}
+	}
+
+	.edit-form {
+		padding: 20px 20px 0;
+		white-space: normal;
+
+		h3 {
+			font-size: 24px;
+			margin-bottom: 8px;
+			line-height: 22px;
+		}
+
+		p {
+			line-height: 18px;
+			font-size: 12px;
+			font-weight: 300;
+		}
+
+		> .input-line {
+			margin-top: 20px;
+		}
+
+		nav {
+			padding: 0;
+
+			&.buttons {
+				margin-top: 2em;
+			}
+		}
+
+		.form-divisor {
+			text-align: center;
+			margin: 2em 0;
+			height: 9px;
+
+			> span {
+				padding: 0 1em;
+			}
+		}
+	}
+
+	.room-info-content > div {
+		margin: 0 0 20px;
+	}
+}
diff --git a/packages/rocketchat-custom-sounds/client/lib/CustomSounds.js b/packages/rocketchat-custom-sounds/client/lib/CustomSounds.js
new file mode 100644
index 00000000000..4d2afb3ad9a
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/client/lib/CustomSounds.js
@@ -0,0 +1,56 @@
+/* globals isSetNotNull */
+class CustomSounds {
+	constructor() {
+		this.list = new ReactiveVar({});
+	}
+
+	add(sound) {
+		sound.src = this.getURL(sound);
+		const audio = $('<audio />', { id: sound._id, preload: true }).append(
+			$('<source />', { src: sound.src })
+		);
+		const list = this.list.get();
+		list[sound._id] = sound;
+		this.list.set(list);
+		$('body').append(audio);
+	}
+
+	remove(sound) {
+		const list = this.list.get();
+		delete this.list[sound._id];
+		this.list.set(list);
+		$('#' + sound._id).remove();
+	}
+
+	update(sound) {
+		const audio = $(`#${sound._id}`);
+		if (audio && audio[0]) {
+			const list = this.list.get();
+			list[sound._id] = sound;
+			this.list.set(list);
+			$('source', audio).attr('src', this.getURL(sound));
+			audio[0].load();
+		} else {
+			this.add(sound);
+		}
+	}
+
+	getURL(sound) {
+		let path = (Meteor.isCordova) ? Meteor.absoluteUrl().replace(/\/$/, '') : __meteor_runtime_config__.ROOT_URL_PATH_PREFIX || '';
+		return `${path}/custom-sounds/${sound._id}.${sound.extension}?_dc=${sound.random || 0}`;
+	}
+
+	getList() {
+		return Object.values(this.list.get());
+	}
+}
+
+RocketChat.CustomSounds = new CustomSounds;
+
+Meteor.startup(() =>
+	Meteor.call('listCustomSounds', (error, result) => {
+		for (let sound of result) {
+			RocketChat.CustomSounds.add(sound);
+		}
+	})
+);
diff --git a/packages/rocketchat-custom-sounds/client/lib/custom-sounds.js b/packages/rocketchat-custom-sounds/client/lib/custom-sounds.js
deleted file mode 100644
index 2273ffea5b2..00000000000
--- a/packages/rocketchat-custom-sounds/client/lib/custom-sounds.js
+++ /dev/null
@@ -1,3 +0,0 @@
-RocketChat.sounds = {
-	list: []
-};
diff --git a/packages/rocketchat-custom-sounds/client/notifications/deleteCustomSound.js b/packages/rocketchat-custom-sounds/client/notifications/deleteCustomSound.js
new file mode 100644
index 00000000000..81323dabc76
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/client/notifications/deleteCustomSound.js
@@ -0,0 +1,3 @@
+Meteor.startup(() =>
+	RocketChat.Notifications.onAll('deleteCustomSound', data => RocketChat.CustomSounds.remove(data.soundData))
+);
diff --git a/packages/rocketchat-custom-sounds/client/notifications/updateCustomSound.js b/packages/rocketchat-custom-sounds/client/notifications/updateCustomSound.js
new file mode 100644
index 00000000000..73b16ae4aa9
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/client/notifications/updateCustomSound.js
@@ -0,0 +1,3 @@
+Meteor.startup(() =>
+	RocketChat.Notifications.onAll('updateCustomSound', data => RocketChat.CustomSounds.update(data.soundData))
+);
diff --git a/packages/rocketchat-custom-sounds/package.js b/packages/rocketchat-custom-sounds/package.js
index b81cf338dce..37fb34df9c8 100644
--- a/packages/rocketchat-custom-sounds/package.js
+++ b/packages/rocketchat-custom-sounds/package.js
@@ -12,6 +12,7 @@ Package.onUse(function(api) {
 		'rocketchat:file',
 		'rocketchat:lib',
 		'templating',
+		'reactive-var',
 		'underscore',
 		'webapp'
 	]);
@@ -27,6 +28,15 @@ Package.onUse(function(api) {
 	api.addFiles('server/models/CustomSounds.js', 'server');
 	api.addFiles('server/publications/customSounds.js', 'server');
 
+	api.addFiles([
+		'server/methods/deleteCustomSound.js',
+		'server/methods/insertOrUpdateSound.js',
+		'server/methods/listCustomSounds.js',
+		'server/methods/uploadCustomSound.js'
+	], 'server');
+
+	api.addFiles('assets/stylesheets/customSoundsAdmin.less', 'client');
+
 	api.addFiles('admin/startup.js', 'client');
 	api.addFiles('admin/adminSounds.html', 'client');
 	api.addFiles('admin/adminSounds.js', 'client');
@@ -34,8 +44,12 @@ Package.onUse(function(api) {
 	api.addFiles('admin/adminSoundInfo.html', 'client');
 	api.addFiles('admin/soundEdit.html', 'client');
 	api.addFiles('admin/soundEdit.js', 'client');
+	api.addFiles('admin/soundInfo.html', 'client');
+	api.addFiles('admin/soundInfo.js', 'client');
 	api.addFiles('admin/route.js', 'client');
 
-	api.addFiles('client/lib/custom-sounds.js', 'client');
+	api.addFiles('client/lib/CustomSounds.js', 'client');
 	api.addFiles('client/models/CustomSounds.js', 'client');
+	api.addFiles('client/notifications/updateCustomSound.js', 'client');
+	api.addFiles('client/notifications/deleteCustomSound.js', 'client');
 });
diff --git a/packages/rocketchat-custom-sounds/server/methods/deleteCustomSound.js b/packages/rocketchat-custom-sounds/server/methods/deleteCustomSound.js
new file mode 100644
index 00000000000..b63c7d03857
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/server/methods/deleteCustomSound.js
@@ -0,0 +1,22 @@
+/* globals isSetNotNull, RocketChatFileCustomSoundsInstance */
+Meteor.methods({
+	deleteCustomSound(_id) {
+		let sound = null;
+
+		if (RocketChat.authz.hasPermission(this.userId, 'manage-sounds')) {
+			sound = RocketChat.models.CustomSounds.findOneByID(_id);
+		} else {
+			throw new Meteor.Error('not_authorized');
+		}
+
+		if (!isSetNotNull(() => sound)) {
+			throw new Meteor.Error('Custom_Sound_Error_Invalid_Sound', 'Invalid sound', { method: 'deleteCustomSound' });
+		}
+
+		RocketChatFileCustomSoundsInstance.deleteFile(`${sound._id}.${sound.extension}`);
+		RocketChat.models.CustomSounds.removeByID(_id);
+		RocketChat.Notifications.notifyAll('deleteCustomSound', {soundData: sound});
+
+		return true;
+	}
+});
diff --git a/packages/rocketchat-custom-sounds/server/methods/insertOrUpdateSound.js b/packages/rocketchat-custom-sounds/server/methods/insertOrUpdateSound.js
new file mode 100644
index 00000000000..845bda2cec7
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/server/methods/insertOrUpdateSound.js
@@ -0,0 +1,62 @@
+/* globals RocketChatFileCustomSoundsInstance */
+Meteor.methods({
+	insertOrUpdateSound(soundData) {
+		if (!RocketChat.authz.hasPermission(this.userId, 'manage-sounds')) {
+			throw new Meteor.Error('not_authorized');
+		}
+
+		if (!s.trim(soundData.name)) {
+			throw new Meteor.Error('error-the-field-is-required', 'The field Name is required', { method: 'insertOrUpdateSound', field: 'Name' });
+		}
+
+		//let nameValidation = new RegExp('^[0-9a-zA-Z-_+;.]+$');
+
+		//allow all characters except colon, whitespace, comma, >, <, &, ", ', /, \, (, )
+		//more practical than allowing specific sets of characters; also allows foreign languages
+		let nameValidation = /[\s,:><&"'\/\\\(\)]/;
+
+		//silently strip colon; this allows for uploading :soundname: as soundname
+		soundData.name = soundData.name.replace(/:/g, '');
+
+		if (nameValidation.test(soundData.name)) {
+			throw new Meteor.Error('error-input-is-not-a-valid-field', `${soundData.name} is not a valid name`, { method: 'insertOrUpdateSound', input: soundData.name, field: 'Name' });
+		}
+
+		let matchingResults = [];
+
+		if (soundData._id) {
+			matchingResults = RocketChat.models.CustomSounds.findByNameExceptID(soundData.name, soundData._id).fetch();
+		} else {
+			matchingResults = RocketChat.models.CustomSounds.findByName(soundData.name).fetch();
+		}
+
+		if (matchingResults.length > 0) {
+			throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', { method: 'insertOrUpdateSound' });
+		}
+
+		if (!soundData._id) {
+			//insert sound
+			let createSound = {
+				name: soundData.name,
+				extension: soundData.extension
+			};
+
+			let _id = RocketChat.models.CustomSounds.create(createSound);
+			createSound._id = _id;
+
+			return _id;
+		} else {
+			//update sound
+			if (soundData.newFile) {
+				RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.previousExtension}`);
+			}
+
+			if (soundData.name !== soundData.previousName) {
+				RocketChat.models.CustomSounds.setName(soundData._id, soundData.name);
+				RocketChat.Notifications.notifyAll('updateCustomSound', {soundData});
+			}
+
+			return soundData._id;
+		}
+	}
+});
diff --git a/packages/rocketchat-custom-sounds/server/methods/listCustomSounds.js b/packages/rocketchat-custom-sounds/server/methods/listCustomSounds.js
new file mode 100644
index 00000000000..e16afd389f9
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/server/methods/listCustomSounds.js
@@ -0,0 +1,5 @@
+Meteor.methods({
+	listCustomSounds() {
+		return RocketChat.models.CustomSounds.find({}).fetch();
+	}
+});
diff --git a/packages/rocketchat-custom-sounds/server/methods/uploadCustomSound.js b/packages/rocketchat-custom-sounds/server/methods/uploadCustomSound.js
new file mode 100644
index 00000000000..9929871559a
--- /dev/null
+++ b/packages/rocketchat-custom-sounds/server/methods/uploadCustomSound.js
@@ -0,0 +1,20 @@
+/* globals RocketChatFileCustomSoundsInstance */
+Meteor.methods({
+	uploadCustomSound(binaryContent, contentType, soundData) {
+		if (!RocketChat.authz.hasPermission(this.userId, 'manage-sounds')) {
+			throw new Meteor.Error('not_authorized');
+		}
+
+		let file = new Buffer(binaryContent, 'binary');
+
+		let rs = RocketChatFile.bufferToStream(file);
+		RocketChatFileCustomSoundsInstance.deleteFile(`${soundData._id}.${soundData.extension}`);
+		let ws = RocketChatFileCustomSoundsInstance.createWriteStream(`${soundData._id}.${soundData.extension}`, contentType);
+		ws.on('end', Meteor.bindEnvironment(() =>
+			Meteor.setTimeout(() => RocketChat.Notifications.notifyAll('updateCustomSound', {soundData})
+			, 500)
+		));
+
+		rs.pipe(ws);
+	}
+});
diff --git a/packages/rocketchat-custom-sounds/server/models/CustomSounds.js b/packages/rocketchat-custom-sounds/server/models/CustomSounds.js
index e205072abca..45f742ce5ac 100644
--- a/packages/rocketchat-custom-sounds/server/models/CustomSounds.js
+++ b/packages/rocketchat-custom-sounds/server/models/CustomSounds.js
@@ -19,6 +19,15 @@ class CustomSounds extends RocketChat.models._Base {
 		return this.find(query, options);
 	}
 
+	findByNameExceptID(name, except, options) {
+		let query = {
+			_id: { $nin: [ except ] },
+			name
+		};
+
+		return this.find(query, options);
+	}
+
 	//update
 	setName(_id, name) {
 		let update = {
diff --git a/packages/rocketchat-custom-sounds/server/publications/customSounds.js b/packages/rocketchat-custom-sounds/server/publications/customSounds.js
index 8b105fe1227..03032018d73 100644
--- a/packages/rocketchat-custom-sounds/server/publications/customSounds.js
+++ b/packages/rocketchat-custom-sounds/server/publications/customSounds.js
@@ -4,7 +4,8 @@ Meteor.publish('customSounds', function(filter, limit) {
 	}
 
 	let fields = {
-		name: 1
+		name: 1,
+		extension: 1
 	};
 
 	filter = s.trim(filter);
diff --git a/packages/rocketchat-custom-sounds/server/startup/custom-sounds.js b/packages/rocketchat-custom-sounds/server/startup/custom-sounds.js
index 502f8bc8712..0f8c10a5240 100644
--- a/packages/rocketchat-custom-sounds/server/startup/custom-sounds.js
+++ b/packages/rocketchat-custom-sounds/server/startup/custom-sounds.js
@@ -1,4 +1,4 @@
-/* globals isSetNotNull */
+/* globals isSetNotNull, RocketChatFileCustomSoundsInstance */
 Meteor.startup(function() {
 	let storeType = 'GridFS';
 
@@ -39,41 +39,13 @@ Meteor.startup(function() {
 			return;
 		}
 
-		let file = self.RocketChatFileCustomSoundsInstance.getFileWithReadStream(encodeURIComponent(params.sound));
-
-		res.setHeader('Content-Disposition', 'inline');
-
-		if (!isSetNotNull(() => file)) {
-			//use code from username initials renderer until file upload is complete
-			res.setHeader('Content-Type', 'image/svg+xml');
-			res.setHeader('Cache-Control', 'public, max-age=0');
-			res.setHeader('Expires', '-1');
-			res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT');
-
-			let reqModifiedHeader = req.headers['if-modified-since'];
-			if (reqModifiedHeader != null) {
-				if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') {
-					res.writeHead(304);
-					res.end();
-					return;
-				}
-			}
-
-			let color = '#000';
-			let initials = '?';
-
-			let svg = `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="50" height="50" style="width: 50px; height: 50px; background-color: ${color};">
-	<text text-anchor="middle" y="50%" x="50%" dy="0.36em" pointer-events="auto" fill="#ffffff" font-family="Helvetica, Arial, Lucida Grande, sans-serif" style="font-weight: 400; font-size: 28px;">
-		${initials}
-	</text>
-</svg>`;
-
-			res.write(svg);
-			res.end();
+		let file = RocketChatFileCustomSoundsInstance.getFileWithReadStream(params.sound);
+		if (!file) {
 			return;
 		}
 
+		res.setHeader('Content-Disposition', 'inline');
+
 		let fileUploadDate = undefined;
 		if (isSetNotNull(() => file.uploadDate)) {
 			fileUploadDate = file.uploadDate.toUTCString();
diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json
index 0ecf988416a..9f849d18d9e 100644
--- a/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -329,14 +329,13 @@
   "Custom_Script_Logged_In": "Custom Script for logged in users",
   "Custom_Script_Logged_Out": "Custom Script for logged out users",
   "Custom_Sounds": "Custom Sounds",
-  "Custom_Sounds_Add": "Add Custom Sound",
-  "Custom_Sounds_Added_Successfully": "Custom sound added successfully",
-  "Custom_Sounds_Delete_Warning": "Deleting a sound cannot be undone.",
-  "Custom_Sounds_Error_Invalid_Sound": "Invalid sound",
-  "Custom_Sounds_Error_Name_Already_In_Use": "The custom sound name is already in use.",
-  "Custom_Sounds_Has_Been_Deleted": "The custom sound has been deleted.",
-  "Custom_Sounds_Info": "Custom Sound Info",
-  "Custom_Sounds_Updated_Successfully": "Custom sound updated successfully",
+  "Custom_Sound_Add": "Add Custom Sound",
+  "Custom_Sound_Delete_Warning": "Deleting a sound cannot be undone.",
+  "Custom_Sound_Error_Invalid_Sound": "Invalid sound",
+  "Custom_Sound_Error_Name_Already_In_Use": "The custom sound name is already in use.",
+  "Custom_Sound_Has_Been_Deleted": "The custom sound has been deleted.",
+  "Custom_Sound_Info": "Custom Sound Info",
+  "Custom_Sound_Saved_Successfully": "Custom sound saved successfully",
   "Custom_Translations": "Custom Translations",
   "Custom_Translations_Description": "Should be a valid JSON where keys are languages containing a dictionary of key and translations. Example:</br><code>{\n\t\"en\": {\n\t\t\"key\": \"translation\"\n\t},\n\t\"pt\": {\n\t\t\"key\": \"tradução\"\n\t}\n}</code> ",
   "CustomSoundsFilesystem": "Custom Sounds Filesystem",
@@ -1245,7 +1244,7 @@
   "Snippet_Messages": "Snippet Messages",
   "Snippeted_a_message": "Created a snippet __snippetLink__",
   "Sound": "Sound",
-  "Sound_File": "Sound File",
+  "Sound_File_mp3": "Sound File (mp3)",
   "SSL": "SSL",
   "Star_Message": "Star Message",
   "Starred_Messages": "Starred Messages",
diff --git a/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js b/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
index 0a5df266957..c553c3a8eaa 100644
--- a/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
+++ b/packages/rocketchat-push-notifications/client/views/pushNotificationsFlexTab.js
@@ -1,9 +1,9 @@
 import toastr from 'toastr';
-/* globals ChatSubscription, KonchatNotification */
+/* globals ChatSubscription */
 
 Template.pushNotificationsFlexTab.helpers({
 	audioAssets() {
-		return KonchatNotification.audioAssets;
+		return RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList() || [];
 	},
 	audioNotifications() {
 		const sub = ChatSubscription.findOne({
@@ -13,7 +13,7 @@ Template.pushNotificationsFlexTab.helpers({
 				audioNotifications: 1
 			}
 		});
-		return sub ? sub.audioNotifications : '';
+		return sub ? sub.audioNotifications || '' : '';
 	},
 	desktopNotifications() {
 		var sub = ChatSubscription.findOne({
@@ -91,7 +91,7 @@ Template.pushNotificationsFlexTab.helpers({
 				audioNotifications: 1
 			}
 		});
-		const audio = sub ? sub.audioNotifications : '';
+		const audio = sub ? sub.audioNotifications || '': '';
 		if (audio === 'none') {
 			return t('None');
 		} else if (audio === '') {
@@ -99,7 +99,8 @@ Template.pushNotificationsFlexTab.helpers({
 		} else if (audio === 'chime') {
 			return 'Chime';
 		} else {
-			const asset = _.findWhere(KonchatNotification.audioAssets, { _id: audio });
+			const audioAssets = RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList() || [];
+			const asset = _.findWhere(audioAssets, { _id: audio });
 			return asset && asset.name;
 		}
 	},
@@ -234,6 +235,14 @@ Template.pushNotificationsFlexTab.events({
 			if ($audio && $audio[0] && $audio[0].play) {
 				$audio[0].play();
 			}
+		} else {
+			audio = Meteor.user() && Meteor.user().settings && Meteor.user().settings.preferences && Meteor.user().settings.preferences.audioNotifications || 'chime';
+			if (audio && audio !== 'none') {
+				let $audio = $('#' + audio);
+				if ($audio && $audio[0] && $audio[0].play) {
+					$audio[0].play();
+				}
+			}
 		}
 	},
 
diff --git a/packages/rocketchat-ui-account/account/accountPreferences.coffee b/packages/rocketchat-ui-account/account/accountPreferences.coffee
index 128f88fa672..ba5a08d1445 100644
--- a/packages/rocketchat-ui-account/account/accountPreferences.coffee
+++ b/packages/rocketchat-ui-account/account/accountPreferences.coffee
@@ -1,7 +1,7 @@
 import toastr from 'toastr'
 Template.accountPreferences.helpers
 	audioAssets: ->
-		return KonchatNotification.audioAssets
+		return RocketChat.CustomSounds && RocketChat.CustomSounds.getList && RocketChat.CustomSounds.getList() || [];
 
 	audioNotifications: ->
 		return Meteor.user()?.settings?.preferences?.audioNotifications || 'chime'
diff --git a/packages/rocketchat-ui/lib/notification.coffee b/packages/rocketchat-ui/lib/notification.coffee
index 75c4d1dd921..e7b72b46237 100644
--- a/packages/rocketchat-ui/lib/notification.coffee
+++ b/packages/rocketchat-ui/lib/notification.coffee
@@ -2,16 +2,6 @@
 @KonchatNotification =
 	notificationStatus: new ReactiveVar
 
-	audioAssets: [
-		{ '_id': 'beep', 'name': 'Beep', 'sources': [ { 'src': 'sounds/beep.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'ding', 'name': 'Ding', 'sources': [ { 'src': 'sounds/ding.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'seasons', 'name': 'Seasons', 'sources': [ { 'src': 'sounds/seasons.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'chelle', 'name': 'Chelle', 'sources': [ { 'src': 'sounds/chelle.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'highbell', 'name': 'High Bell', 'sources': [ { 'src': 'sounds/highbell.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'droplet', 'name': 'Droplet', 'sources': [ { 'src': 'sounds/droplet.mp3', 'type': 'audio/mpeg' } ] }
-		{ '_id': 'verbal', 'name': 'Verbal', 'sources': [ { 'src': 'sounds/verbal.mp3', 'type': 'audio/mpeg' } ] }
-	]
-
 	# notificacoes HTML5
 	getDesktopPermission: ->
 		if window.Notification && Notification.permission != "granted" && !Meteor.settings.public.sandstorm
diff --git a/packages/rocketchat-ui/package.js b/packages/rocketchat-ui/package.js
index 90231eaa764..72f8da9b1a6 100644
--- a/packages/rocketchat-ui/package.js
+++ b/packages/rocketchat-ui/package.js
@@ -78,7 +78,6 @@ Package.onUse(function(api) {
 	api.addFiles('views/404/roomNotFound.html', 'client');
 	api.addFiles('views/404/invalidSecretURL.html', 'client');
 	api.addFiles('views/app/audioNotification.html', 'client');
-	api.addFiles('views/app/audioNotification.js', 'client');
 	api.addFiles('views/app/burger.html', 'client');
 	api.addFiles('views/app/home.html', 'client');
 	api.addFiles('views/app/notAuthorized.html', 'client');
diff --git a/packages/rocketchat-ui/views/app/audioNotification.html b/packages/rocketchat-ui/views/app/audioNotification.html
index a9f36904cca..ec4106f7756 100644
--- a/packages/rocketchat-ui/views/app/audioNotification.html
+++ b/packages/rocketchat-ui/views/app/audioNotification.html
@@ -1,15 +1,10 @@
 <template name="audioNotification">
-	<audio id="chime" preload>
-		<source src="sounds/chime.mp3" type="audio/mpeg" />
-	</audio>
-	<audio id="chatNewRoomNotification" preload>
-		<source src="sounds/door.mp3" type="audio/mpeg" />
-	</audio>
-	{{#each audioAssets}}
-		<audio id="{{_id}}" preload>
-			{{#each sources}}
-				<source src="{{src}}" type="{{type}}" />
-			{{/each}}
+	<div id="audioFilesPreload">
+		<audio id="chime" preload>
+			<source src="sounds/chime.mp3" type="audio/mpeg" />
 		</audio>
-	{{/each}}
+		<audio id="chatNewRoomNotification" preload>
+			<source src="sounds/door.mp3" type="audio/mpeg" />
+		</audio>
+	</div>
 </template>
diff --git a/packages/rocketchat-ui/views/app/audioNotification.js b/packages/rocketchat-ui/views/app/audioNotification.js
deleted file mode 100644
index c0f1ccdd8b9..00000000000
--- a/packages/rocketchat-ui/views/app/audioNotification.js
+++ /dev/null
@@ -1,6 +0,0 @@
-/* globals KonchatNotification */
-Template.audioNotification.helpers({
-	audioAssets() {
-		return KonchatNotification.audioAssets;
-	}
-})
-- 
GitLab