Skip to content

Commit 2ad1fc6

Browse files
committed
feat(files_sharing): show Account menu on public pages
Signed-off-by: skjnldsv <skjnldsv@protonmail.com>
1 parent ddcaa8d commit 2ad1fc6

10 files changed

Lines changed: 257 additions & 27 deletions

File tree

apps/files_sharing/src/public-file-request.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55

66
import { defineAsyncComponent } from 'vue'
77
import { getBuilder } from '@nextcloud/browser-storage'
8-
import { getGuestNickname, setGuestNickname } from '@nextcloud/auth'
8+
import { getGuestNickname } from '@nextcloud/auth'
99
import { getUploader } from '@nextcloud/upload'
10+
import { loadState } from '@nextcloud/initial-state'
1011
import { spawnDialog } from '@nextcloud/dialogs'
12+
import { t } from '@nextcloud/l10n'
1113

1214
import logger from './services/logger'
1315

@@ -28,10 +30,6 @@ function registerFileRequestHeader(nickname: string) {
2830
* @param nickname The chosen nickname
2931
*/
3032
function onSetNickname(nickname: string): void {
31-
// Set the nickname
32-
setGuestNickname(nickname)
33-
// Set the dialog as shown
34-
storage.setItem('public-auth-prompt-shown', 'true')
3533
// Register header for uploader
3634
registerFileRequestHeader(nickname)
3735
}
@@ -40,14 +38,38 @@ window.addEventListener('DOMContentLoaded', () => {
4038
const nickname = getGuestNickname() ?? ''
4139
const dialogShown = storage.getItem('public-auth-prompt-shown') !== null
4240

41+
const owner = loadState('files_sharing', 'owner', '')
42+
const ownerDisplayName = loadState('files_sharing', 'ownerDisplayName', '')
43+
const label = loadState('files_sharing', 'label', '')
44+
const filename = loadState('files_sharing', 'filename', '')
45+
46+
// If the owner provided a custom label, use it instead of the filename
47+
const folder = label || filename
48+
49+
const options = {
50+
nickname,
51+
notice: t('files_sharing', 'To upload files to {folder}, you need to provide your name first.', { folder }),
52+
subtitle: undefined as string | undefined,
53+
title: t('files_sharing', 'Upload files to {folder}', { folder }),
54+
}
55+
56+
// If the guest already has a nickname, we just make them double check
57+
if (nickname) {
58+
options.notice = t('files_sharing', 'Please confirm your name to upload files to {folder}', { folder })
59+
}
60+
61+
// If the account owner set their name as public,
62+
// we show it in the subtitle
63+
if (owner) {
64+
options.subtitle = t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName })
65+
}
66+
4367
// If we don't have a nickname or the public auth prompt hasn't been shown yet, show it
4468
// We still show the prompt if the user has a nickname to double check
4569
if (!nickname || !dialogShown) {
4670
spawnDialog(
4771
defineAsyncComponent(() => import('./views/PublicAuthPrompt.vue')),
48-
{
49-
nickname,
50-
},
72+
options,
5173
onSetNickname as (...rest: unknown[]) => void,
5274
)
5375
} else {

apps/files_sharing/src/views/PublicAuthPrompt.vue

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
data-cy-public-auth-prompt-dialog
1010
is-form
1111
:can-close="false"
12-
:name="dialogName"
13-
@submit="$emit('close', name)">
14-
<p v-if="owner" class="public-auth-prompt__subtitle">
15-
{{ t('files_sharing', '{ownerDisplayName} shared a folder with you.', { ownerDisplayName }) }}
12+
:name="title"
13+
@submit="onSubmit">
14+
<p v-if="subtitle" class="public-auth-prompt__subtitle">
15+
{{ subtitle }}
1616
</p>
1717

1818
<!-- Header -->
1919
<NcNoteCard class="public-auth-prompt__header"
20-
:text="t('files_sharing', 'To upload files, you need to provide your name first.')"
20+
:text="notice"
2121
type="info" />
2222

2323
<!-- Form -->
@@ -35,12 +35,15 @@
3535

3636
<script lang="ts">
3737
import { defineComponent } from 'vue'
38+
import { getBuilder } from '@nextcloud/browser-storage'
39+
import { setGuestNickname } from '@nextcloud/auth'
3840
import { t } from '@nextcloud/l10n'
3941
4042
import NcDialog from '@nextcloud/vue/components/NcDialog'
4143
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
4244
import NcTextField from '@nextcloud/vue/components/NcTextField'
43-
import { loadState } from '@nextcloud/initial-state'
45+
46+
const storage = getBuilder('files_sharing').build()
4447
4548
export default defineComponent({
4649
name: 'PublicAuthPrompt',
@@ -60,17 +63,37 @@ export default defineComponent({
6063
type: String,
6164
default: '',
6265
},
66+
67+
/**
68+
* Dialog title
69+
*/
70+
title: {
71+
type: String,
72+
default: t('files_sharing', 'Guest identification'),
73+
},
74+
75+
/**
76+
* Dialog subtitle
77+
* @default 'Enter your name to access the file'
78+
*/
79+
subtitle: {
80+
type: String,
81+
default: '',
82+
},
83+
84+
/**
85+
* Dialog notice
86+
* @default 'You are currently not identified.'
87+
*/
88+
notice: {
89+
type: String,
90+
default: t('files_sharing', 'You are currently not identified.'),
91+
},
6392
},
6493
6594
setup() {
6695
return {
6796
t,
68-
69-
owner: loadState('files_sharing', 'owner', ''),
70-
ownerDisplayName: loadState('files_sharing', 'ownerDisplayName', ''),
71-
label: loadState('files_sharing', 'label', ''),
72-
note: loadState('files_sharing', 'note', ''),
73-
filename: loadState('files_sharing', 'filename', ''),
7497
}
7598
},
7699
@@ -81,9 +104,6 @@ export default defineComponent({
81104
},
82105
83106
computed: {
84-
dialogName() {
85-
return this.t('files_sharing', 'Upload files to {folder}', { folder: this.label || this.filename })
86-
},
87107
dialogButtons() {
88108
return [{
89109
label: t('files_sharing', 'Submit name'),
@@ -102,6 +122,21 @@ export default defineComponent({
102122
immediate: true,
103123
},
104124
},
125+
126+
methods: {
127+
onSubmit() {
128+
const nickname = this.name.trim()
129+
130+
// Set the nickname
131+
setGuestNickname(nickname)
132+
133+
// Set the dialog as shown
134+
storage.setItem('public-auth-prompt-shown', 'true')
135+
136+
// Close the dialog
137+
this.$emit('close', this.name)
138+
},
139+
},
105140
})
106141
</script>
107142
<style scoped lang="scss">

core/src/components/PublicPageMenu/PublicPageMenuEntry.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,24 @@
1111
role="presentation"
1212
@click="$emit('click')">
1313
<template #icon>
14-
<div role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" />
14+
<slot v-if="$scopedSlots.icon" name="icon" />
15+
<div v-else role="presentation" :class="['icon', icon, 'public-page-menu-entry__icon']" />
1516
</template>
1617
</NcListItem>
1718
</template>
1819

1920
<script setup lang="ts">
20-
import NcListItem from '@nextcloud/vue/components/NcListItem'
2121
import { onMounted } from 'vue'
2222
23+
import NcListItem from '@nextcloud/vue/components/NcListItem'
24+
2325
const props = defineProps<{
2426
/** Only emit click event but do not open href */
2527
clickOnly?: boolean
2628
// menu entry props
2729
id: string
2830
label: string
29-
icon: string
31+
icon?: string
3032
href: string
3133
details?: string
3234
}>()

core/src/public-page-user-menu.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
import { getCSPNonce } from '@nextcloud/auth'
7+
import Vue from 'vue'
8+
9+
import PublicPageUserMenu from './views/PublicPageUserMenu.vue'
10+
11+
__webpack_nonce__ = getCSPNonce()
12+
13+
const View = Vue.extend(PublicPageUserMenu)
14+
const instance = new View()
15+
instance.$mount('#public-page-user-menu')

core/src/views/AccountMenu.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ export default defineComponent({
211211
}
212212
}
213213
214-
// Ensure we do not wast space, as the header menu sets a default width of 350px
214+
// Ensure we do not waste space, as the header menu sets a default width of 350px
215215
:deep(.header-menu__content) {
216216
width: fit-content !important;
217217
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<!--
2+
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
3+
- SPDX-License-Identifier: AGPL-3.0-or-later
4+
-->
5+
<template>
6+
<NcHeaderMenu id="public-page-user-menu"
7+
class="public-page-user-menu"
8+
is-nav
9+
:aria-label="t('core', 'User menu')"
10+
:description="avatarDescription">
11+
<template #trigger>
12+
<NcAvatar class="public-page-user-menu__avatar"
13+
disable-menu
14+
disable-tooltip
15+
is-guest
16+
:user="currentUser || '?'" />
17+
</template>
18+
<ul class="public-page-user-menu__list">
19+
<!-- Privacy notice -->
20+
<NcNoteCard class="public-page-user-menu__list-note"
21+
:text="privacyNotice"
22+
type="info" />
23+
24+
<!-- Nickname dialog -->
25+
<AccountMenuEntry id="set-nickname"
26+
:name="!currentUser ? t('core', 'Set public name') : t('core', 'Change public name')"
27+
href="#"
28+
@click.capture.prevent.stop="setNickname">
29+
<template #icon>
30+
<IconAccount />
31+
</template>
32+
</AccountMenuEntry>
33+
</ul>
34+
</NcHeaderMenu>
35+
</template>
36+
37+
<script lang="ts">
38+
import { defineAsyncComponent, defineComponent } from 'vue'
39+
import { getGuestNickname } from '@nextcloud/auth'
40+
import { spawnDialog } from '@nextcloud/dialogs'
41+
import { t } from '@nextcloud/l10n'
42+
43+
import NcAvatar from '@nextcloud/vue/components/NcAvatar'
44+
import NcHeaderMenu from '@nextcloud/vue/components/NcHeaderMenu'
45+
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
46+
import IconAccount from 'vue-material-design-icons/Account.vue'
47+
48+
import AccountMenuEntry from '../components/AccountMenu/AccountMenuEntry.vue'
49+
50+
export default defineComponent({
51+
name: 'PublicPageUserMenu',
52+
components: {
53+
AccountMenuEntry,
54+
IconAccount,
55+
NcAvatar,
56+
NcHeaderMenu,
57+
NcNoteCard,
58+
},
59+
60+
setup() {
61+
return {
62+
t,
63+
}
64+
},
65+
66+
data() {
67+
return {
68+
currentUser: getGuestNickname(),
69+
}
70+
},
71+
72+
computed: {
73+
avatarDescription(): string {
74+
return t('core', 'User menu')
75+
},
76+
77+
privacyNotice(): string {
78+
return this.currentUser
79+
? t('core', 'You will be identified as {user} by the account owner.', { user: this.currentUser })
80+
: t('core', 'You are currently not identified.')
81+
},
82+
},
83+
84+
methods: {
85+
setNickname() {
86+
spawnDialog(
87+
defineAsyncComponent(() => import('../../../apps/files_sharing/src/views/PublicAuthPrompt.vue')),
88+
{
89+
nickname: this.currentUser,
90+
},
91+
((newName: string) => { this.currentUser = newName }) as (...rest: unknown[]) => void,
92+
)
93+
},
94+
},
95+
})
96+
</script>
97+
98+
<style scoped lang="scss">
99+
.public-page-user-menu {
100+
box-sizing: border-box;
101+
102+
// Ensure we do not waste space, as the header menu sets a default width of 350px
103+
:deep(.header-menu__content) {
104+
width: fit-content !important;
105+
}
106+
107+
&__list-note {
108+
padding-block: 5px !important;
109+
padding-inline: 5px !important;
110+
max-width: 300px;
111+
margin: 5px !important;
112+
margin-bottom: 0 !important;
113+
}
114+
115+
&__list {
116+
display: inline-flex;
117+
flex-direction: column;
118+
padding-block: var(--default-grid-baseline) 0;
119+
padding-inline: 0 var(--default-grid-baseline);
120+
121+
> :deep(li) {
122+
box-sizing: border-box;
123+
// basically "fit-content"
124+
flex: 0 1;
125+
}
126+
}
127+
}
128+
</style>

core/templates/layout.public.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777

7878
<div class="header-end">
7979
<div id="public-page-menu"></div>
80+
<div id="public-page-user-menu"></div>
8081
</div>
8182
</header>
8283

lib/public/AppFramework/Http/Template/PublicTemplateResponse.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public function __construct(
4444
) {
4545
parent::__construct($appName, $templateName, $params, 'public', $status, $headers);
4646
\OCP\Util::addScript('core', 'public-page-menu');
47+
\OCP\Util::addScript('core', 'public-page-user-menu');
4748

4849
$state = \OCP\Server::get(IInitialStateService::class);
4950
$state->provideLazyInitialState('core', 'public-page-menu', function () {

0 commit comments

Comments
 (0)