diff --git a/.gitattributes b/.gitattributes
index fbcb8a02f0d47f56544c2b39408979c9de73b5b9..6afffcdb16dba71b82340ef1966b2c786b678400 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -5,6 +5,8 @@
 /apps/accessibility/js/accessibility.js.map binary
 /apps/comments/js/*.js binary
 /apps/comments/js/*.js.map binary
+/apps/dashboard/js/*.js binary
+/apps/dashboard/js/*.js.map binary
 /apps/files/js/dist/*.js binary
 /apps/files/js/dist/*.js.map binary
 /apps/files_sharing/js/dist/*.js binary
diff --git a/apps/dashboard/appinfo/routes.php b/apps/dashboard/appinfo/routes.php
index 34792a9d47dbd1cb367a849a8c03de03643c2577..4edca1a3ec56d966a5bc6d61ced61fc5bbf01b28 100644
--- a/apps/dashboard/appinfo/routes.php
+++ b/apps/dashboard/appinfo/routes.php
@@ -27,5 +27,6 @@ declare(strict_types=1);
 return [
 	'routes' => [
 		['name' => 'dashboard#index', 'url' => '/', 'verb' => 'GET'],
+		['name' => 'dashboard#updateLayout', 'url' => '/layout', 'verb' => 'POST'],
 	]
 ];
diff --git a/apps/dashboard/js/dashboard.js b/apps/dashboard/js/dashboard.js
index 650d57b54be00e0633b267cc9ff76747f1707518..03e6ced23819a5a625a8009277f3d20d5033b35a 100644
Binary files a/apps/dashboard/js/dashboard.js and b/apps/dashboard/js/dashboard.js differ
diff --git a/apps/dashboard/js/dashboard.js.map b/apps/dashboard/js/dashboard.js.map
index ac0af8ab77ba418f7fdc89c30e72637e2b89424e..5c0b96d6fb31456445dbebaa9c1bc903ca0b73f3 100644
Binary files a/apps/dashboard/js/dashboard.js.map and b/apps/dashboard/js/dashboard.js.map differ
diff --git a/apps/dashboard/lib/Controller/DashboardController.php b/apps/dashboard/lib/Controller/DashboardController.php
index 687fbace380d04a1226d52aae298a994c7f4609f..305759fa6e8e5b7a00af8b4e92fdc9f005ecb09d 100644
--- a/apps/dashboard/lib/Controller/DashboardController.php
+++ b/apps/dashboard/lib/Controller/DashboardController.php
@@ -26,13 +26,16 @@ declare(strict_types=1);
 
 namespace OCA\Dashboard\Controller;
 
+use OCA\Files\Event\LoadSidebar;
 use OCA\Viewer\Event\LoadViewer;
 use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http\JSONResponse;
 use OCP\AppFramework\Http\TemplateResponse;
 use OCP\Dashboard\IManager;
-use OCP\Dashboard\IPanel;
-use OCP\Dashboard\RegisterPanelEvent;
+use OCP\Dashboard\IWidget;
+use OCP\Dashboard\RegisterWidgetEvent;
 use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IConfig;
 use OCP\IInitialStateService;
 use OCP\IRequest;
 
@@ -44,19 +47,27 @@ class DashboardController extends Controller {
 	private $eventDispatcher;
 	/** @var IManager */
 	private $dashboardManager;
+	/** @var IConfig */
+	private $config;
+	/** @var string */
+	private $userId;
 
 	public function __construct(
 		string $appName,
 		IRequest $request,
 		IInitialStateService $initialStateService,
 		IEventDispatcher $eventDispatcher,
-		IManager $dashboardManager
+		IManager $dashboardManager,
+		IConfig $config,
+		$userId
 	) {
 		parent::__construct($appName, $request);
 
 		$this->inititalStateService = $initialStateService;
 		$this->eventDispatcher = $eventDispatcher;
 		$this->dashboardManager = $dashboardManager;
+		$this->config = $config;
+		$this->userId = $userId;
 	}
 
 	/**
@@ -65,23 +76,37 @@ class DashboardController extends Controller {
 	 * @return TemplateResponse
 	 */
 	public function index(): TemplateResponse {
-		$this->eventDispatcher->dispatchTyped(new RegisterPanelEvent($this->dashboardManager));
-
-		$dashboardManager = $this->dashboardManager;
-		$panels = array_map(function (IPanel $panel) {
-			return [
-				'id' => $panel->getId(),
-				'title' => $panel->getTitle(),
-				'iconClass' => $panel->getIconClass(),
-				'url' => $panel->getUrl()
-			];
-		}, $dashboardManager->getPanels());
-		$this->inititalStateService->provideInitialState('dashboard', 'panels', $panels);
-
+		$this->eventDispatcher->dispatchTyped(new LoadSidebar());
 		if (class_exists(LoadViewer::class)) {
 			$this->eventDispatcher->dispatchTyped(new LoadViewer());
 		}
 
+		$this->eventDispatcher->dispatchTyped(new RegisterWidgetEvent($this->dashboardManager));
+
+		$userLayout = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', 'recommendations,spreed,mail,calendar'));
+		$widgets = array_map(function (IWidget $widget) {
+			return [
+				'id' => $widget->getId(),
+				'title' => $widget->getTitle(),
+				'iconClass' => $widget->getIconClass(),
+				'url' => $widget->getUrl()
+			];
+		}, $this->dashboardManager->getWidgets());
+		$this->inititalStateService->provideInitialState('dashboard', 'panels', $widgets);
+		$this->inititalStateService->provideInitialState('dashboard', 'layout', $userLayout);
+		$this->inititalStateService->provideInitialState('dashboard', 'firstRun', $this->config->getUserValue($this->userId, 'dashboard', 'firstRun', '1') === '1');
+		$this->config->setUserValue($this->userId, 'dashboard', 'firstRun', '0');
+
 		return new TemplateResponse('dashboard', 'index');
 	}
+
+	/**
+	 * @NoAdminRequired
+	 * @param string $layout
+	 * @return JSONResponse
+	 */
+	public function updateLayout(string $layout): JSONResponse {
+		$this->config->setUserValue($this->userId, 'dashboard', 'layout', $layout);
+		return new JSONResponse(['layout' => $layout]);
+	}
 }
diff --git a/apps/dashboard/src/App.vue b/apps/dashboard/src/App.vue
index 87c76a603b43496370b326606cf74b6037ec4975..d73f895fc9141157a1dd95a5a0a1f4dfdd59a6c5 100644
--- a/apps/dashboard/src/App.vue
+++ b/apps/dashboard/src/App.vue
@@ -1,17 +1,57 @@
 <template>
 	<div id="app-dashboard">
 		<h2>{{ greeting.icon }} {{ greeting.text }}</h2>
+		<div class="statuses">
+			<div v-for="status in registeredStatus"
+				:id="'status-' + status"
+				:key="status"
+				:ref="'status-' + status" />
+		</div>
 
-		<div class="panels">
-			<div v-for="panel in panels" :key="panel.id" class="panel">
-				<a :href="panel.url">
-					<h3 :class="panel.iconClass">
-						{{ panel.title }}
+		<Draggable v-model="layout"
+			class="panels"
+			handle=".panel--header"
+			@end="saveLayout">
+			<div v-for="panelId in layout" :key="panels[panelId].id" class="panel">
+				<div class="panel--header">
+					<h3 :class="panels[panelId].iconClass">
+						{{ panels[panelId].title }}
 					</h3>
-				</a>
-				<div :ref="panel.id" :data-id="panel.id" />
+				</div>
+				<div class="panel--content">
+					<div :ref="panels[panelId].id" :data-id="panels[panelId].id" />
+				</div>
 			</div>
-		</div>
+		</Draggable>
+
+		<a v-tooltip="tooltip"
+			class="edit-panels icon-add"
+			:class="{ firstrun: firstRun }"
+			@click="showModal">{{ t('dashboard', 'Edit widgets') }}</a>
+
+		<Modal v-if="modal" @close="closeModal">
+			<div class="modal__content">
+				<h3>{{ t('dashboard', 'Edit widgets') }}</h3>
+				<Draggable v-model="layout"
+					class="panels"
+					tag="ol"
+					handle=".draggable"
+					@end="saveLayout">
+					<li v-for="panel in sortedPanels" :key="panel.id">
+						<input :id="'panel-checkbox-' + panel.id"
+							type="checkbox"
+							class="checkbox"
+							:checked="isActive(panel)"
+							@input="updateCheckbox(panel, $event.target.checked)">
+						<label :for="'panel-checkbox-' + panel.id" :class="isActive(panel) ? 'draggable ' + panel.iconClass : panel.iconClass">
+							{{ panel.title }}
+						</label>
+					</li>
+				</Draggable>
+
+				<a :href="appStoreUrl" class="button">{{ t('dashboard', 'Get more widgets from the app store') }}</a>
+			</div>
+		</Modal>
 	</div>
 </template>
 
@@ -19,49 +59,93 @@
 import Vue from 'vue'
 import { loadState } from '@nextcloud/initial-state'
 import { getCurrentUser } from '@nextcloud/auth'
+import { Modal } from '@nextcloud/vue'
+import Draggable from 'vuedraggable'
+import axios from '@nextcloud/axios'
+import { generateUrl } from '@nextcloud/router'
 
 const panels = loadState('dashboard', 'panels')
+const firstRun = loadState('dashboard', 'firstRun')
 
 export default {
 	name: 'App',
+	components: {
+		Modal,
+		Draggable,
+	},
 	data() {
 		return {
 			timer: new Date(),
+			registeredStatus: [],
 			callbacks: {},
+			callbacksStatus: {},
 			panels,
-			name: getCurrentUser()?.displayName,
+			firstRun,
+			displayName: getCurrentUser()?.displayName,
+			uid: getCurrentUser()?.uid,
+			layout: loadState('dashboard', 'layout').filter((panelId) => panels[panelId]),
+			modal: false,
+			appStoreUrl: generateUrl('/settings/apps/dashboard'),
+			statuses: {},
 		}
 	},
 	computed: {
+		tooltip() {
+			if (!this.firstRun) {
+				return null
+			}
+			return {
+				content: t('dashboard', 'Adjust the dashboard to your needs'),
+				placement: 'top',
+				show: true,
+				trigger: 'manual',
+			}
+		},
 		greeting() {
 			const time = this.timer.getHours()
+			const shouldShowName = this.displayName && this.uid !== this.displayName
 
 			if (time > 18) {
-				return { icon: '🌙', text: t('dashboard', 'Good evening, {name}', { name: this.name }) }
+				return { icon: '🌙', text: shouldShowName ? t('dashboard', 'Good evening, {name}', { name: this.displayName }) : t('dashboard', 'Good evening') }
 			}
 			if (time > 12) {
-				return { icon: '☀', text: t('dashboard', 'Good afternoon, {name}', { name: this.name }) }
+				return { icon: '☀', text: shouldShowName ? t('dashboard', 'Good afternoon, {name}', { name: this.displayName }) : t('dashboard', 'Good afternoon') }
 			}
 			if (time === 12) {
-				return { icon: '🍽', text: t('dashboard', 'Time for lunch, {name}', { name: this.name }) }
+				return { icon: '🍽', text: shouldShowName ? t('dashboard', 'Time for lunch, {name}', { name: this.displayName }) : t('dashboard', 'Time for lunch') }
 			}
 			if (time > 5) {
-				return { icon: '🌄', text: t('dashboard', 'Good morning, {name}', { name: this.name }) }
+				return { icon: '🌄', text: shouldShowName ? t('dashboard', 'Good morning, {name}', { name: this.displayName }) : t('dashboard', 'Good morning') }
 			}
-			return { icon: '🦉', text: t('dashboard', 'Have a night owl, {name}', { name: this.name }) }
+			return { icon: '🦉', text: shouldShowName ? t('dashboard', 'Have a night owl, {name}', { name: this.displayName }) : t('dashboard', 'Have a night owl') }
+		},
+		isActive() {
+			return (panel) => this.layout.indexOf(panel.id) > -1
+		},
+		sortedPanels() {
+			return Object.values(this.panels).sort((a, b) => {
+				const indexA = this.layout.indexOf(a.id)
+				const indexB = this.layout.indexOf(b.id)
+				if (indexA === -1 || indexB === -1) {
+					return indexB - indexA || a.id - b.id
+				}
+				return indexA - indexB || a.id - b.id
+			})
 		},
 	},
 	watch: {
 		callbacks() {
-			for (const app in this.callbacks) {
-				const element = this.$refs[app]
-				if (this.panels[app].mounted) {
+			this.rerenderPanels()
+		},
+		callbacksStatus() {
+			for (const app in this.callbacksStatus) {
+				const element = this.$refs['status-' + app]
+				if (this.statuses[app] && this.statuses[app].mounted) {
 					continue
 				}
-
 				if (element) {
-					this.callbacks[app](element[0])
-					Vue.set(this.panels[app], 'mounted', true)
+					this.callbacksStatus[app](element[0])
+					Vue.set(this.statuses, app, { mounted: true })
 				} else {
 					console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
 				}
@@ -72,11 +156,74 @@ export default {
 		setInterval(() => {
 			this.timer = new Date()
 		}, 30000)
+
+		if (this.firstRun) {
+			window.addEventListener('scroll', this.disableFirstrunHint)
+		}
 	},
 	methods: {
+		/**
+		 * Method to register panels that will be called by the integrating apps
+		 *
+		 * @param {string} app The unique app id for the widget
+		 * @param {function} callback The callback function to register a panel which gets the DOM element passed as parameter
+		 */
 		register(app, callback) {
 			Vue.set(this.callbacks, app, callback)
 		},
+		registerStatus(app, callback) {
+			this.registeredStatus.push(app)
+			this.$nextTick(() => {
+				Vue.set(this.callbacksStatus, app, callback)
+			})
+		},
+		rerenderPanels() {
+			for (const app in this.callbacks) {
+				const element = this.$refs[app]
+				if (this.layout.indexOf(app) === -1) {
+					continue
+				}
+				if (this.panels[app] && this.panels[app].mounted) {
+					continue
+				}
+				if (element) {
+					this.callbacks[app](element[0])
+					Vue.set(this.panels[app], 'mounted', true)
+				} else {
+					console.error('Failed to register panel in the frontend as no backend data was provided for ' + app)
+				}
+			}
+		},
+		saveLayout() {
+			axios.post(generateUrl('/apps/dashboard/layout'), {
+				layout: this.layout.join(','),
+			})
+		},
+		showModal() {
+			this.modal = true
+			this.firstRun = false
+		},
+		closeModal() {
+			this.modal = false
+		},
+		updateCheckbox(panel, currentValue) {
+			const index = this.layout.indexOf(panel.id)
+			if (!currentValue && index > -1) {
+				this.layout.splice(index, 1)
+
+			} else {
+				this.layout.push(panel.id)
+			}
+			Vue.set(this.panels[panel.id], 'mounted', false)
+			this.saveLayout()
+			this.$nextTick(() => this.rerenderPanels())
+		},
+		disableFirstrunHint() {
+			window.removeEventListener('scroll', this.disableFirstrunHint)
+			setTimeout(() => {
+				this.firstRun = false
+			}, 1000)
+		},
 	},
 }
 </script>
@@ -84,16 +231,20 @@ export default {
 <style lang="scss" scoped>
 	#app-dashboard {
 		width: 100%;
+		margin-bottom: 100px;
 	}
+
 	h2 {
 		text-align: center;
 		font-size: 32px;
 		line-height: 130%;
-		padding: 80px 16px 32px;
+		padding: 80px 16px 0px;
 	}
 
 	.panels {
-		width: 100%;
+		width: auto;
+		margin: auto;
+		max-width: 1500px;
 		display: flex;
 		justify-content: center;
 		flex-direction: row;
@@ -101,26 +252,125 @@ export default {
 		flex-wrap: wrap;
 	}
 
-	.panel {
-		width: 250px;
+	.panel, .panels > div {
+		width: 320px;
+		max-width: 100%;
 		margin: 16px;
+		background-color: var(--color-main-background-translucent);
+		border-radius: var(--border-radius-large);
+		border: 2px solid var(--color-border);
 
-		& > a {
-			position: sticky;
+		&.sortable-ghost {
+			 opacity: 0.1;
+		}
+
+		& > .panel--header {
+			display: flex;
+			z-index: 1;
 			top: 50px;
-			display: block;
-			background: linear-gradient(var(--color-main-background-translucent), var(--color-main-background-translucent) 80%, rgba(255, 255, 255, 0));
+			padding: 16px;
+			// TO DO: use variables here
+			background: linear-gradient(170deg, rgba(0, 130,201, 0.2) 0%, rgba(255,255,255,.1) 50%, rgba(255,255,255,0) 100%);
+			border-top-left-radius: calc(var(--border-radius-large) - 2px);
+			border-top-right-radius: calc(var(--border-radius-large) - 2px);
 			backdrop-filter: blur(4px);
+			cursor: grab;
+
+			&, ::v-deep * {
+				-webkit-touch-callout: none;
+				-webkit-user-select: none;
+				-khtml-user-select: none;
+				-moz-user-select: none;
+				-ms-user-select: none;
+				user-select: none;
+			}
+
+			&:active {
+				cursor: grabbing;
+			}
+
+			a {
+				flex-grow: 1;
+			}
 
 			h3 {
+				display: block;
+				flex-grow: 1;
 				margin: 0;
 				font-size: 20px;
 				font-weight: bold;
 				background-size: 32px;
-				background-position: 10px 10px;
-				padding: 16px 8px 16px 52px;
+				background-position: 14px 12px;
+				padding: 16px 8px 16px 60px;
+				cursor: grab;
 			}
 		}
+
+		& > .panel--content {
+			margin: 0 16px 16px 16px;
+			height: 420px;
+			overflow: auto;
+		}
+	}
+
+	.edit-panels {
+		z-index: 99;
+		position: fixed;
+		bottom: 20px;
+		right: 20px;
+		padding: 10px 15px 10px 35px;
+		background-position: 10px center;
+		opacity: .7;
+		background-color: var(--color-main-background);
+		border-radius: var(--border-radius-pill);
+		transition: right var(--animation-slow) ease-in-out;
+
+		&:hover {
+			opacity: 1;
+			background-color: var(--color-background-hover);
+		}
+
+		&.firstrun {
+			right: 50%;
+			transform: translateX(50%);
+			max-width: 200px;
+			box-shadow: 0px 0px 3px var(--color-box-shadow);
+			opacity: 1;
+			text-align: center;
+		}
+	}
+
+	.modal__content {
+		width: 30vw;
+		margin: 20px;
+		ol {
+			display: flex;
+			flex-direction: column;
+			list-style-type: none;
+		}
+		li label {
+			padding: 10px;
+			display: block;
+			list-style-type: none;
+			background-size: 16px;
+			background-position: left center;
+			padding-left: 26px;
+		}
+	}
+
+	.flip-list-move {
+		transition: transform var(--animation-slow);
+	}
+
+	.statuses {
+		display: flex;
+		flex-direction: row;
+		justify-content: center;
+		margin-bottom: 40px;
+
+		& > div {
+			max-width: 200px;
+		}
 	}
 
 </style>
diff --git a/apps/dashboard/src/main.js b/apps/dashboard/src/main.js
index 998f538356bc7df7582a6e7ea6d1cabc2603c9e5..17619cc22bf6ecc9f4957ceaa859c1ab0533fd13 100644
--- a/apps/dashboard/src/main.js
+++ b/apps/dashboard/src/main.js
@@ -1,9 +1,19 @@
 import Vue from 'vue'
 import App from './App.vue'
+import { translate as t } from '@nextcloud/l10n'
+import VTooltip from '@nextcloud/vue/dist/Directives/Tooltip'
+
+Vue.directive('Tooltip', VTooltip)
+
+Vue.prototype.t = t
+
+// FIXME workaround to make the sidebar work
+Object.assign(window.OCA.Files, { App: { fileList: { filesClient: OC.Files.getClient() } } }, window.OCA.Files)
 
 const Dashboard = Vue.extend(App)
-const Instance = new Dashboard({}).$mount('#app')
+const Instance = new Dashboard({}).$mount('#app-content-vue')
 
 window.OCA.Dashboard = {
 	register: (app, callback) => Instance.register(app, callback),
+	registerStatus: (app, callback) => Instance.registerStatus(app, callback),
 }
diff --git a/apps/dashboard/templates/index.php b/apps/dashboard/templates/index.php
index bca9fa30494682d0dcf6789e8f50aeee96450579..e4056ce06e26903e55d03d26664e251d84eb517a 100644
--- a/apps/dashboard/templates/index.php
+++ b/apps/dashboard/templates/index.php
@@ -1,4 +1,4 @@
 <?php
 	\OCP\Util::addScript('dashboard', 'dashboard');
 ?>
-<div id="app"></div>
+<div id="app-content-vue"></div>
diff --git a/apps/user_status/js/user-status-menu.js b/apps/user_status/js/user-status-menu.js
index 6c8889898c83c09e5bad39ce1137859626c8de86..865fcc1b3b5e39b35a669737c5aa736ac4c5a524 100644
Binary files a/apps/user_status/js/user-status-menu.js and b/apps/user_status/js/user-status-menu.js differ
diff --git a/apps/user_status/js/user-status-menu.js.map b/apps/user_status/js/user-status-menu.js.map
index 4b866db86448ad3ce3d2f8d9cc7a4e33f1ec72ef..77b8fc4702d0a729d1b46e0f8646f3b25f997306 100644
Binary files a/apps/user_status/js/user-status-menu.js.map and b/apps/user_status/js/user-status-menu.js.map differ
diff --git a/apps/user_status/src/App.vue b/apps/user_status/src/App.vue
index 116c7a0ca4701517aa176ab6f0bacaa77121d970..c9e95fa87fd10879a1485dbc5413bd352667466f 100644
--- a/apps/user_status/src/App.vue
+++ b/apps/user_status/src/App.vue
@@ -20,9 +20,10 @@
   -->
 
 <template>
-	<li>
+	<li :class="{ inline }">
 		<div id="user-status-menu-item">
 			<span
+				v-if="!inline"
 				id="user-status-menu-item__header"
 				:title="displayName">
 				{{ displayName }}
@@ -71,6 +72,12 @@ export default {
 		ActionButton,
 		SetStatusModal,
 	},
+	props: {
+		inline: {
+			type: Boolean,
+			default: false,
+		},
+	},
 	data() {
 		return {
 			isModalOpen: false,
@@ -237,7 +244,7 @@ export default {
 </script>
 
 <style lang="scss">
-#user-status-menu-item {
+li:not(.inline) #user-status-menu-item {
 	&__header {
 		display: block;
 		color: var(--color-main-text);
@@ -270,4 +277,33 @@ export default {
 		}
 	}
 }
+
+.inline #user-status-menu-item__subheader {
+	width: 100%;
+
+	> button {
+		background-color: var(--color-main-background);
+		background-size: 16px;
+		border: 0;
+		border-radius: var(--border-radius-pill);
+		font-weight: normal;
+		font-size: 0.875em;
+		padding-left: 40px;
+
+		&:hover,
+		&:focus {
+			background-color: var(--color-background-hover);
+		}
+
+		&.icon-loading-small {
+			&::after {
+				left: 21px;
+			}
+		}
+	}
+}
+
+	li {
+		list-style-type: none;
+	}
 </style>
diff --git a/apps/user_status/src/main-user-status-menu.js b/apps/user_status/src/main-user-status-menu.js
index 795f41df4e765f87f9c799027be7fba03723d863..c6d23337526ce4b3cfabe6a931af53ad9726b3bf 100644
--- a/apps/user_status/src/main-user-status-menu.js
+++ b/apps/user_status/src/main-user-status-menu.js
@@ -20,4 +20,20 @@ const app = new Vue({
 	store,
 }).$mount('li[data-id="user_status-menuitem"]')
 
+document.addEventListener('DOMContentLoaded', function() {
+	if (!OCA.Dashboard) {
+		return
+	}
+
+	OCA.Dashboard.registerStatus('status', (el) => {
+		const Dashboard = Vue.extend(App)
+		return new Dashboard({
+			propsData: {
+				inline: true,
+			},
+			store,
+		}).$mount(el)
+	})
+})
+
 export { app }
diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php
index 5f61b848a8322f56f44dfd26321353930534a29b..0a64301f13bbde835c20b8900bb31107774b2d40 100644
--- a/lib/composer/composer/autoload_classmap.php
+++ b/lib/composer/composer/autoload_classmap.php
@@ -170,13 +170,13 @@ return array(
     'OCP\\Dashboard\\IDashboardManager' => $baseDir . '/lib/public/Dashboard/IDashboardManager.php',
     'OCP\\Dashboard\\IDashboardWidget' => $baseDir . '/lib/public/Dashboard/IDashboardWidget.php',
     'OCP\\Dashboard\\IManager' => $baseDir . '/lib/public/Dashboard/IManager.php',
-    'OCP\\Dashboard\\IPanel' => $baseDir . '/lib/public/Dashboard/IPanel.php',
+    'OCP\\Dashboard\\IWidget' => $baseDir . '/lib/public/Dashboard/IWidget.php',
     'OCP\\Dashboard\\Model\\IWidgetConfig' => $baseDir . '/lib/public/Dashboard/Model/IWidgetConfig.php',
     'OCP\\Dashboard\\Model\\IWidgetRequest' => $baseDir . '/lib/public/Dashboard/Model/IWidgetRequest.php',
     'OCP\\Dashboard\\Model\\WidgetSetting' => $baseDir . '/lib/public/Dashboard/Model/WidgetSetting.php',
     'OCP\\Dashboard\\Model\\WidgetSetup' => $baseDir . '/lib/public/Dashboard/Model/WidgetSetup.php',
     'OCP\\Dashboard\\Model\\WidgetTemplate' => $baseDir . '/lib/public/Dashboard/Model/WidgetTemplate.php',
-    'OCP\\Dashboard\\RegisterPanelEvent' => $baseDir . '/lib/public/Dashboard/RegisterPanelEvent.php',
+    'OCP\\Dashboard\\RegisterWidgetEvent' => $baseDir . '/lib/public/Dashboard/RegisterWidgetEvent.php',
     'OCP\\Dashboard\\Service\\IEventsService' => $baseDir . '/lib/public/Dashboard/Service/IEventsService.php',
     'OCP\\Dashboard\\Service\\IWidgetsService' => $baseDir . '/lib/public/Dashboard/Service/IWidgetsService.php',
     'OCP\\Defaults' => $baseDir . '/lib/public/Defaults.php',
diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php
index a37769e9ed603ca4ee42cf8bc27eed698c62b619..43c45b2fe834f4798f2e41c0006329cb69dd6178 100644
--- a/lib/composer/composer/autoload_static.php
+++ b/lib/composer/composer/autoload_static.php
@@ -199,13 +199,13 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
         'OCP\\Dashboard\\IDashboardManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IDashboardManager.php',
         'OCP\\Dashboard\\IDashboardWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IDashboardWidget.php',
         'OCP\\Dashboard\\IManager' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IManager.php',
-        'OCP\\Dashboard\\IPanel' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IPanel.php',
+        'OCP\\Dashboard\\IWidget' => __DIR__ . '/../../..' . '/lib/public/Dashboard/IWidget.php',
         'OCP\\Dashboard\\Model\\IWidgetConfig' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/IWidgetConfig.php',
         'OCP\\Dashboard\\Model\\IWidgetRequest' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/IWidgetRequest.php',
         'OCP\\Dashboard\\Model\\WidgetSetting' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetSetting.php',
         'OCP\\Dashboard\\Model\\WidgetSetup' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetSetup.php',
         'OCP\\Dashboard\\Model\\WidgetTemplate' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Model/WidgetTemplate.php',
-        'OCP\\Dashboard\\RegisterPanelEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterPanelEvent.php',
+        'OCP\\Dashboard\\RegisterWidgetEvent' => __DIR__ . '/../../..' . '/lib/public/Dashboard/RegisterWidgetEvent.php',
         'OCP\\Dashboard\\Service\\IEventsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IEventsService.php',
         'OCP\\Dashboard\\Service\\IWidgetsService' => __DIR__ . '/../../..' . '/lib/public/Dashboard/Service/IWidgetsService.php',
         'OCP\\Defaults' => __DIR__ . '/../../..' . '/lib/public/Defaults.php',
diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
index efcf9175b978e9fd02e0305e8eccc197889e05e4..4c37209739e5620e219febc723ce3fa7be3f38d2 100644
--- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php
+++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php
@@ -100,10 +100,10 @@ class RegistrationContext {
 				);
 			}
 
-			public function registerDashboardPanel(string $panelClass): void {
+			public function registerDashboardWidget(string $widgetClass): void {
 				$this->context->registerDashboardPanel(
 					$this->appId,
-					$panelClass
+					$widgetClass
 				);
 			}
 
@@ -282,7 +282,7 @@ class RegistrationContext {
 	public function delegateDashboardPanelRegistrations(array $apps, IManager $dashboardManager): void {
 		foreach ($this->dashboardPanels as $panel) {
 			try {
-				$dashboardManager->lazyRegisterPanel($panel['class']);
+				$dashboardManager->lazyRegisterWidget($panel['class']);
 			} catch (Throwable $e) {
 				$appId = $panel['appId'];
 				$this->logger->logException($e, [
diff --git a/lib/private/Dashboard/Manager.php b/lib/private/Dashboard/Manager.php
index 0c285a8b53d1fa3be21d3b21648af0325445efe8..fda4c8b3893179a85c893576d9d5c416b0d367c8 100644
--- a/lib/private/Dashboard/Manager.php
+++ b/lib/private/Dashboard/Manager.php
@@ -29,7 +29,7 @@ namespace OC\Dashboard;
 use InvalidArgumentException;
 use OCP\AppFramework\QueryException;
 use OCP\Dashboard\IManager;
-use OCP\Dashboard\IPanel;
+use OCP\Dashboard\IWidget;
 use OCP\ILogger;
 use OCP\IServerContainer;
 use Throwable;
@@ -37,10 +37,10 @@ use Throwable;
 class Manager implements IManager {
 
 	/** @var array */
-	private $lazyPanels = [];
+	private $lazyWidgets = [];
 
-	/** @var IPanel[] */
-	private $panels = [];
+	/** @var IWidget[] */
+	private $widgets = [];
 
 	/** @var IServerContainer */
 	private $serverContainer;
@@ -49,31 +49,31 @@ class Manager implements IManager {
 		$this->serverContainer = $serverContainer;
 	}
 
-	private function registerPanel(IPanel $panel): void {
-		if (array_key_exists($panel->getId(), $this->panels)) {
-			throw new InvalidArgumentException('Dashboard panel with this id has already been registered');
+	private function registerWidget(IWidget $widget): void {
+		if (array_key_exists($widget->getId(), $this->widgets)) {
+			throw new InvalidArgumentException('Dashboard widget with this id has already been registered');
 		}
 
-		$this->panels[$panel->getId()] = $panel;
+		$this->widgets[$widget->getId()] = $widget;
 	}
 
-	public function lazyRegisterPanel(string $panelClass): void {
-		$this->lazyPanels[] = $panelClass;
+	public function lazyRegisterWidget(string $widgetClass): void {
+		$this->lazyWidgets[] = $widgetClass;
 	}
 
 	public function loadLazyPanels(): void {
-		$classes = $this->lazyPanels;
+		$classes = $this->lazyWidgets;
 		foreach ($classes as $class) {
 			try {
-				/** @var IPanel $panel */
-				$panel = $this->serverContainer->query($class);
+				/** @var IWidget $widget */
+				$widget = $this->serverContainer->query($class);
 			} catch (QueryException $e) {
 				/*
 				 * There is a circular dependency between the logger and the registry, so
 				 * we can not inject it. Thus the static call.
 				 */
 				\OC::$server->getLogger()->logException($e, [
-					'message' => 'Could not load lazy dashbaord panel: ' . $e->getMessage(),
+					'message' => 'Could not load lazy dashbaord widget: ' . $e->getMessage(),
 					'level' => ILogger::FATAL,
 				]);
 			}
@@ -82,32 +82,32 @@ class Manager implements IManager {
 			 * type, so we might get a TypeError here that we should catch.
 			 */
 			try {
-				$this->registerPanel($panel);
+				$this->registerWidget($widget);
 			} catch (Throwable $e) {
 				/*
 				 * There is a circular dependency between the logger and the registry, so
 				 * we can not inject it. Thus the static call.
 				 */
 				\OC::$server->getLogger()->logException($e, [
-					'message' => 'Could not register lazy dashboard panel: ' . $e->getMessage(),
+					'message' => 'Could not register lazy dashboard widget: ' . $e->getMessage(),
 					'level' => ILogger::FATAL,
 				]);
 			}
 
 			try {
-				$panel->load();
+				$widget->load();
 			} catch (Throwable $e) {
 				\OC::$server->getLogger()->logException($e, [
-					'message' => 'Error during dashboard panel loading: ' . $e->getMessage(),
+					'message' => 'Error during dashboard widget loading: ' . $e->getMessage(),
 					'level' => ILogger::FATAL,
 				]);
 			}
 		}
-		$this->lazyPanels = [];
+		$this->lazyWidgets = [];
 	}
 
-	public function getPanels(): array {
+	public function getWidgets(): array {
 		$this->loadLazyPanels();
-		return $this->panels;
+		return $this->widgets;
 	}
 }
diff --git a/lib/private/legacy/OC_Util.php b/lib/private/legacy/OC_Util.php
index 82b7abf6c8f04b927878de9a2ae50e574de098ff..ab386ab61727ce8a1856a3fa6440ef193c286045 100644
--- a/lib/private/legacy/OC_Util.php
+++ b/lib/private/legacy/OC_Util.php
@@ -68,6 +68,7 @@ use OCP\IConfig;
 use OCP\IGroupManager;
 use OCP\ILogger;
 use OCP\IUser;
+use OCP\IUserSession;
 
 class OC_Util {
 	public static $scripts = [];
@@ -1088,6 +1089,8 @@ class OC_Util {
 	 * @suppress PhanDeprecatedFunction
 	 */
 	public static function getDefaultPageUrl() {
+		/** @var IConfig $config */
+		$config = \OC::$server->get(IConfig::class);
 		$urlGenerator = \OC::$server->getURLGenerator();
 		// Deny the redirect if the URL contains a @
 		// This prevents unvalidated redirects like ?redirect_url=:user@domain.com
@@ -1099,8 +1102,16 @@ class OC_Util {
 				$location = $urlGenerator->getAbsoluteURL($defaultPage);
 			} else {
 				$appId = 'files';
-				$config = \OC::$server->getConfig();
-				$defaultApps = explode(',', $config->getSystemValue('defaultapp', 'files'));
+				$defaultApps = explode(',', $config->getSystemValue('defaultapp', 'dashboard,files'));
+
+				/** @var IUserSession $userSession */
+				$userSession = \OC::$server->get(IUserSession::class);
+				$user = $userSession->getUser();
+				if ($user) {
+					$userDefaultApps = explode(',', $config->getUserValue($user->getUID(), 'core', 'defaultapp'));
+					$defaultApps = array_filter(array_merge($userDefaultApps, $defaultApps));
+				}
+
 				// find the first app that is enabled for the current user
 				foreach ($defaultApps as $defaultApp) {
 					$defaultApp = OC_App::cleanAppId(strip_tags($defaultApp));
diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
index 94e3aed17e2e0037c159cacbe7c33b5b9d816587..0acf0c038bbccddca67d8ee506a2870c5eec44d2 100644
--- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
+++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php
@@ -59,11 +59,11 @@ interface IRegistrationContext {
 	 * Register an implementation of \OCP\Dashboard\IPanel that
 	 * will handle the implementation of a dashboard panel
 	 *
-	 * @param string $panelClass
+	 * @param string $widgetClass
 	 * @return void
 	 * @since 20.0.0
 	 */
-	public function registerDashboardPanel(string $panelClass): void;
+	public function registerDashboardWidget(string $widgetClass): void;
 	/**
 	 * Register a service
 	 *
diff --git a/lib/public/Dashboard/IManager.php b/lib/public/Dashboard/IManager.php
index 985c8c7838f876162caff04162ac7b4025965ae6..81b1bb0dffee7fcc02f026a69350300924fadc88 100644
--- a/lib/public/Dashboard/IManager.php
+++ b/lib/public/Dashboard/IManager.php
@@ -35,15 +35,15 @@ namespace OCP\Dashboard;
 interface IManager {
 
 	/**
-	 * @param string $panelClass
+	 * @param string $widgetClass
 	 * @since 20.0.0
 	 */
-	public function lazyRegisterPanel(string $panelClass): void;
+	public function lazyRegisterWidget(string $widgetClass): void;
 
 	/**
 	 * @since 20.0.0
 	 *
-	 * @return IPanel[]
+	 * @return IWidget[]
 	 */
-	public function getPanels(): array;
+	public function getWidgets(): array;
 }
diff --git a/lib/public/Dashboard/IPanel.php b/lib/public/Dashboard/IWidget.php
similarity index 79%
rename from lib/public/Dashboard/IPanel.php
rename to lib/public/Dashboard/IWidget.php
index 59d88f7a7e991a0d48c33dd2beff260746ab6894..42e2c7df5b20b0c05c2903cfd354965257265c7c 100644
--- a/lib/public/Dashboard/IPanel.php
+++ b/lib/public/Dashboard/IWidget.php
@@ -27,33 +27,33 @@ declare(strict_types=1);
 namespace OCP\Dashboard;
 
 /**
- * Interface IPanel
+ * Interface IWidget
  *
  * @package OCP\Dashboard
  * @since 20.0.0
  */
-interface IPanel {
+interface IWidget {
 
 	/**
-	 * @return string Unique id that identifies the panel, e.g. the app id
+	 * @return string Unique id that identifies the widget, e.g. the app id
 	 * @since 20.0.0
 	 */
 	public function getId(): string;
 
 	/**
-	 * @return string User facing title of the panel
+	 * @return string User facing title of the widget
 	 * @since 20.0.0
 	 */
 	public function getTitle(): string;
 
 	/**
-	 * @return int Initial order for panel sorting
+	 * @return int Initial order for widget sorting
 	 * @since 20.0.0
 	 */
 	public function getOrder(): int;
 
 	/**
-	 * @return string css class that displays an icon next to the panel title
+	 * @return string css class that displays an icon next to the widget title
 	 * @since 20.0.0
 	 */
 	public function getIconClass(): string;
@@ -65,7 +65,7 @@ interface IPanel {
 	public function getUrl(): ?string;
 
 	/**
-	 * Execute panel bootstrap code like loading scripts and providing initial state
+	 * Execute widget bootstrap code like loading scripts and providing initial state
 	 * @since 20.0.0
 	 */
 	public function load(): void;
diff --git a/lib/public/Dashboard/RegisterPanelEvent.php b/lib/public/Dashboard/RegisterWidgetEvent.php
similarity index 91%
rename from lib/public/Dashboard/RegisterPanelEvent.php
rename to lib/public/Dashboard/RegisterWidgetEvent.php
index 2bd157127fd0c1d5b7d3b1af496673f765303e30..0267a9e0d36ed1f5eba0aacb79552cfb70bbeb78 100644
--- a/lib/public/Dashboard/RegisterPanelEvent.php
+++ b/lib/public/Dashboard/RegisterWidgetEvent.php
@@ -40,7 +40,7 @@ use OCP\EventDispatcher\Event;
  * @since 20.0.0
  * @deprecated 20.0.0
  */
-class RegisterPanelEvent extends Event {
+class RegisterWidgetEvent extends Event {
 	private $manager;
 
 	public function __construct(IManager $manager) {
@@ -53,7 +53,7 @@ class RegisterPanelEvent extends Event {
 	 * @param string $panelClass
 	 * @since 20.0.0
 	 */
-	public function registerPanel(string $panelClass) {
-		$this->manager->lazyRegisterPanel($panelClass);
+	public function registerWidget(string $panelClass) {
+		$this->manager->lazyRegisterWidget($panelClass);
 	}
 }
diff --git a/package-lock.json b/package-lock.json
index ab643d9beaa63718925b4b974add30841902fb79..70cc0d8d0fc39d9fa60ff41a8d6645cf9ec8d9f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8900,6 +8900,11 @@
         }
       }
     },
+    "sortablejs": {
+      "version": "1.10.2",
+      "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.10.2.tgz",
+      "integrity": "sha512-YkPGufevysvfwn5rfdlGyrGjt7/CRHwvRPogD/lC+TnvcN29jDpCifKP+rBqf+LRldfXSTh+0CGLcSg0VIxq3A=="
+    },
     "source-list-map": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz",
@@ -9963,6 +9968,14 @@
         "date-format-parse": "^0.2.5"
       }
     },
+    "vuedraggable": {
+      "version": "2.24.0",
+      "resolved": "https://registry.npmjs.org/vuedraggable/-/vuedraggable-2.24.0.tgz",
+      "integrity": "sha512-IlslPpc+iZ2zPNSJbydFZIDrE+don5u+Nc/bjT2YaF+Azidc+wxxJKfKT0NwE68AKk0syb0YbZneAcnynqREZQ==",
+      "requires": {
+        "sortablejs": "^1.10.1"
+      }
+    },
     "vuex": {
       "version": "3.5.1",
       "resolved": "https://registry.npmjs.org/vuex/-/vuex-3.5.1.tgz",
diff --git a/package.json b/package.json
index e8487b19d2c95ab76b201702277f9896761afbb8..37588737a2c92231a7675ee951d410c264eadfa2 100644
--- a/package.json
+++ b/package.json
@@ -82,6 +82,7 @@
     "vue-material-design-icons": "^4.8.0",
     "vue-multiselect": "^2.1.6",
     "vue-router": "^3.3.4",
+    "vuedraggable": "^2.24.0",
     "vuex": "^3.5.1",
     "vuex-router-sync": "^5.0.0"
   },
diff --git a/resources/app-info-shipped.xsd b/resources/app-info-shipped.xsd
index c120a524844490eddbf74eba47692dfc17c379a5..90ca881c6866de22b574a508e9982f6495f8087f 100644
--- a/resources/app-info-shipped.xsd
+++ b/resources/app-info-shipped.xsd
@@ -341,6 +341,7 @@
 
     <xs:simpleType name="category">
         <xs:restriction base="xs:string">
+            <xs:enumeration value="dashboard"/>
             <xs:enumeration value="security"/>
             <xs:enumeration value="customization"/>
             <xs:enumeration value="files"/>
diff --git a/resources/app-info.xsd b/resources/app-info.xsd
index ae8b170202d79aae09ba3ffe88ade186a31487a2..7c0f04efb591dbf9766703066d9b5abc7db54eae 100644
--- a/resources/app-info.xsd
+++ b/resources/app-info.xsd
@@ -341,6 +341,7 @@
 
     <xs:simpleType name="category">
         <xs:restriction base="xs:string">
+            <xs:enumeration value="dashboard"/>
             <xs:enumeration value="security"/>
             <xs:enumeration value="customization"/>
             <xs:enumeration value="files"/>
diff --git a/tests/acceptance/installAndConfigureServer.sh b/tests/acceptance/installAndConfigureServer.sh
index d24405fa448f7a6b02fa4d90cc36e0e0352c16ec..99d51e951afd180b123372dff8c4bdc0dcc77d6e 100755
--- a/tests/acceptance/installAndConfigureServer.sh
+++ b/tests/acceptance/installAndConfigureServer.sh
@@ -39,6 +39,8 @@ OC_PASS=123456acb php occ user:add --password-from-env user1
 OC_PASS=123456acb php occ user:add --password-from-env disabledUser
 php occ user:disable disabledUser
 
+php occ app:disable dashboard
+
 if [ "$NEXTCLOUD_SERVER_DOMAIN" != "" ]; then
 	# Default first trusted domain is "localhost"; replace it with given domain.
 	php occ config:system:set trusted_domains 0 --value="$NEXTCLOUD_SERVER_DOMAIN"