feat: block action on user pages + shared instance flows

This commit is contained in:
Calum H. (IMB11)
2026-07-28 16:08:25 +01:00
parent 90bfe88dac
commit a8b41382ff
8 changed files with 286 additions and 12 deletions
@@ -120,8 +120,17 @@
:max-height="240" :max-height="240"
/> />
</div> </div>
<div v-if="reportOnly" class="flex flex-col gap-2"> <div v-if="reportOnly || blockTargetUserId" class="flex flex-col gap-2">
<Checkbox v-model="deleteInstance" :label="formatMessage(messages.deleteInstance)" /> <Checkbox
v-if="reportOnly"
v-model="deleteInstance"
:label="formatMessage(messages.deleteInstance)"
/>
<Checkbox
v-if="blockTargetUserId"
v-model="blockUser"
:label="formatMessage(messages.blockUser)"
/>
</div> </div>
</div> </div>
</Transition> </Transition>
@@ -249,12 +258,14 @@ import {
Admonition, Admonition,
AutoLink, AutoLink,
Avatar, Avatar,
blockedUsersQueryKey,
ButtonStyled, ButtonStyled,
Checkbox, Checkbox,
Combobox, Combobox,
type ComboboxOption, type ComboboxOption,
commonMessages, commonMessages,
defineMessages, defineMessages,
injectAuth,
injectModrinthClient, injectModrinthClient,
injectNotificationManager, injectNotificationManager,
IntlFormatted, IntlFormatted,
@@ -266,6 +277,7 @@ import {
useScrollIndicator, useScrollIndicator,
useVIntl, useVIntl,
} from '@modrinth/ui' } from '@modrinth/ui'
import { useQueryClient } from '@tanstack/vue-query'
import { openUrl } from '@tauri-apps/plugin-opener' import { openUrl } from '@tauri-apps/plugin-opener'
import { computed, nextTick, ref } from 'vue' import { computed, nextTick, ref } from 'vue'
@@ -274,6 +286,7 @@ import { hide_ads_window, show_ads_window } from '@/helpers/ads'
import { toError } from '@/helpers/errors' import { toError } from '@/helpers/errors'
import type { SharedInstanceInstallPreview } from '@/helpers/install' import type { SharedInstanceInstallPreview } from '@/helpers/install'
import { create_report } from '@/helpers/reports' import { create_report } from '@/helpers/reports'
import { block_user } from '@/helpers/users'
import SharedInstanceInstallSummary from './shared-instance-install-summary.vue' import SharedInstanceInstallSummary from './shared-instance-install-summary.vue'
import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content' import { useSharedInstancePreviewContent } from './use-shared-instance-preview-content'
@@ -284,6 +297,7 @@ type ExternalFileRow = {
name: string name: string
} }
type SharedInstanceCreator = { type SharedInstanceCreator = {
id: string | null
username: string username: string
avatarUrl: string | null avatarUrl: string | null
} }
@@ -300,13 +314,17 @@ type ReportReason = 'malicious' | 'inappropriate' | 'spam'
const reportReason = ref<ReportReason>('malicious') const reportReason = ref<ReportReason>('malicious')
const additionalContext = ref('') const additionalContext = ref('')
const deleteInstance = ref(true) const deleteInstance = ref(true)
const blockUser = ref(true)
const blockTargetUserId = ref<string | null>(null)
const submitLoading = ref(false) const submitLoading = ref(false)
const uploadedImageIDs = ref<string[]>([]) const uploadedImageIDs = ref<string[]>([])
const emit = defineEmits<{ const emit = defineEmits<{
reported: [deleteInstance: boolean] reported: [deleteInstance: boolean]
}>() }>()
const { formatMessage } = useVIntl() const { formatMessage } = useVIntl()
const auth = injectAuth()
const client = injectModrinthClient() const client = injectModrinthClient()
const queryClient = useQueryClient()
const { addNotification, handleError } = injectNotificationManager() const { addNotification, handleError } = injectNotificationManager()
const { load } = useSharedInstancePreviewContent() const { load } = useSharedInstancePreviewContent()
const { const {
@@ -365,13 +383,41 @@ async function submitReport() {
submitLoading.value = true submitLoading.value = true
try { try {
const uploadedImages = uploadedImageIDs.value.slice(-10) const uploadedImages = uploadedImageIDs.value.slice(-10)
await create_report({ const blockTarget = blockUser.value ? blockTargetUserId.value : null
report_type: reportReason.value, const [reportResult, blockResult] = await Promise.allSettled([
item_type: 'shared-instance', create_report({
item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`, report_type: reportReason.value,
body, item_type: 'shared-instance',
uploaded_images: uploadedImages, item_id: `${reportPreview.sharedInstanceId}/${reportPreview.version}`,
}) body,
uploaded_images: uploadedImages,
}),
blockTarget ? block_user(blockTarget) : Promise.resolve(),
])
if (blockTarget) {
if (blockResult.status === 'fulfilled') {
blockUser.value = false
const authUserId = auth.user.value?.id
if (authUserId) {
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
blockedUsersQueryKey(authUserId),
(blockedUsers = []) =>
blockedUsers.includes(blockTarget)
? blockedUsers
: [...blockedUsers, blockTarget],
)
}
addNotification({
type: 'success',
title: formatMessage(messages.userBlocked),
})
} else {
handleError(toError(blockResult.reason))
}
}
if (reportResult.status === 'rejected') throw reportResult.reason
const shouldDeleteInstance = reportOnly.value && deleteInstance.value const shouldDeleteInstance = reportOnly.value && deleteInstance.value
hide() hide()
@@ -426,6 +472,8 @@ function resetReportState() {
reportReason.value = 'malicious' reportReason.value = 'malicious'
additionalContext.value = '' additionalContext.value = ''
deleteInstance.value = true deleteInstance.value = true
blockUser.value = true
blockTargetUserId.value = null
submitLoading.value = false submitLoading.value = false
uploadedImageIDs.value = [] uploadedImageIDs.value = []
} }
@@ -437,12 +485,18 @@ function show(
) { ) {
resetReportState() resetReportState()
creator.value = creatorValue ?? null creator.value = creatorValue ?? null
blockTargetUserId.value = creatorValue?.id ?? null
install.value = installValue install.value = installValue
showPreview(previewValue, event) showPreview(previewValue, event)
} }
function showReport(previewValue: SharedInstanceInstallPreview, event?: MouseEvent) { function showReport(
previewValue: SharedInstanceInstallPreview,
blockTargetUserIdValue?: string | null,
event?: MouseEvent,
) {
resetReportState() resetReportState()
creator.value = null creator.value = null
blockTargetUserId.value = blockTargetUserIdValue ?? null
reportMode.value = true reportMode.value = true
reportOnly.value = true reportOnly.value = true
install.value = () => {} install.value = () => {}
@@ -537,6 +591,14 @@ const messages = defineMessages({
id: 'app.modal.install-to-play.delete-instance', id: 'app.modal.install-to-play.delete-instance',
defaultMessage: 'Delete instance', defaultMessage: 'Delete instance',
}, },
blockUser: {
id: 'app.modal.install-to-play.block-user',
defaultMessage: 'Block user',
},
userBlocked: {
id: 'app.modal.install-to-play.user-blocked',
defaultMessage: 'User blocked',
},
unknownFilesWarning: { unknownFilesWarning: {
id: 'app.modal.install-to-play.unknown-files-warning', id: 'app.modal.install-to-play.unknown-files-warning',
defaultMessage: 'Unknown files warning', defaultMessage: 'Unknown files warning',
@@ -32,6 +32,7 @@ type InstallModal = {
} }
type SharedInstanceCreator = { type SharedInstanceCreator = {
id: string | null
username: string username: string
avatarUrl: string | null avatarUrl: string | null
} }
@@ -190,6 +191,7 @@ export function useSharedInstanceInviteHandler(
}, },
invite.invitedByUsername invite.invitedByUsername
? { ? {
id: invite.invitedById,
username: invite.invitedByUsername, username: invite.invitedByUsername,
avatarUrl: invite.invitedByAvatarUrl, avatarUrl: invite.invitedByAvatarUrl,
} }
@@ -294,6 +296,7 @@ export function useSharedInstanceInviteHandler(
}, },
manager manager
? { ? {
id: manager.id,
username: manager.username, username: manager.username,
avatarUrl: manager.avatar_url ?? null, avatarUrl: manager.avatar_url ?? null,
} }
+6
View File
@@ -18,11 +18,14 @@ import { computed, watch } from 'vue'
import { onBeforeRouteUpdate, useRoute } from 'vue-router' import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import { import {
block_user,
get_blocked_users,
get_user_collections, get_user_collections,
get_user_organizations, get_user_organizations,
get_user_profile, get_user_profile,
get_user_projects, get_user_projects,
patch_user, patch_user,
unblock_user,
} from '@/helpers/users' } from '@/helpers/users'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
@@ -35,6 +38,9 @@ const userProfile = provideUserProfile({
getOrganizations: get_user_organizations, getOrganizations: get_user_organizations,
getCollections: get_user_collections, getCollections: get_user_collections,
patchUser: patch_user, patchUser: patch_user,
getBlockedUsers: get_blocked_users,
blockUser: block_user,
unblockUser: unblock_user,
}) })
const userId = computed(() => { const userId = computed(() => {
@@ -701,7 +701,7 @@ async function reportSharedInstance(event?: MouseEvent, closeUpdateModal = false
) )
if (instance.value?.id !== reportInstance.id) return if (instance.value?.id !== reportInstance.id) return
if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide() if (closeUpdateModal) sharedInstanceUpdateModal.value?.hide()
sharedInstanceReportModal.value?.showReport(preview, event) sharedInstanceReportModal.value?.showReport(preview, sharedInstance.manager_id, event)
} catch (error) { } catch (error) {
notifySharedInstanceError(error) notifySharedInstanceError(error)
} }
+3
View File
@@ -34,6 +34,9 @@ const userProfile = provideUserProfile({
getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId), getOrganizations: (userId) => client.labrinth.users_v2.getOrganizations(userId),
getCollections: (userId) => client.labrinth.users_v2.getCollections(userId), getCollections: (userId) => client.labrinth.users_v2.getCollections(userId),
patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch), patchUser: (userId, patch) => client.labrinth.users_v2.patch(userId, patch),
getBlockedUsers: () => client.labrinth.blocked_users_v3.list(),
blockUser: (userId) => client.labrinth.blocked_users_v3.block(userId),
unblockUser: (userId) => client.labrinth.blocked_users_v3.unblock(userId),
}) })
const auth = await useAuth() const auth = await useAuth()
const cosmetics = useCosmetics() const cosmetics = useCosmetics()
@@ -82,6 +82,7 @@ import type { Labrinth } from '@modrinth/api-client'
import { import {
AffiliateIcon, AffiliateIcon,
BadgeCheckIcon, BadgeCheckIcon,
BanIcon,
BoxIcon, BoxIcon,
CalendarIcon, CalendarIcon,
ChartIcon, ChartIcon,
@@ -123,6 +124,14 @@ const messages = defineMessages({
id: 'profile.button.billing', id: 'profile.button.billing',
defaultMessage: 'Manage user billing', defaultMessage: 'Manage user billing',
}, },
blockButton: {
id: 'profile.button.block',
defaultMessage: 'Block',
},
unblockButton: {
id: 'profile.button.unblock',
defaultMessage: 'Unblock',
},
editRoleButton: { editRoleButton: {
id: 'profile.button.edit-role', id: 'profile.button.edit-role',
defaultMessage: 'Edit role', defaultMessage: 'Edit role',
@@ -175,6 +184,7 @@ const props = withDefaults(
isAdmin?: boolean isAdmin?: boolean
isStaff?: boolean isStaff?: boolean
showStaffActions?: boolean showStaffActions?: boolean
isBlocked?: boolean
projectsCount?: number projectsCount?: number
downloads?: number downloads?: number
}>(), }>(),
@@ -190,6 +200,7 @@ const props = withDefaults(
isAdmin: false, isAdmin: false,
isStaff: false, isStaff: false,
showStaffActions: false, showStaffActions: false,
isBlocked: false,
projectsCount: 0, projectsCount: 0,
downloads: 0, downloads: 0,
}, },
@@ -198,6 +209,7 @@ const props = withDefaults(
const emit = defineEmits<{ const emit = defineEmits<{
manageProjects: [] manageProjects: []
report: [] report: []
block: []
copyId: [] copyId: []
copyPermalink: [] copyPermalink: []
openBilling: [] openBilling: []
@@ -236,6 +248,14 @@ const moreActions = computed<TeleportOverflowMenuItem[]>(() => [
color: 'red', color: 'red',
shown: props.authUser?.id !== props.user.id, shown: props.authUser?.id !== props.user.id,
}, },
{
id: 'block',
label: formatMessage(props.isBlocked ? messages.unblockButton : messages.blockButton),
icon: BanIcon,
action: () => emit('block'),
color: 'red',
shown: props.authUser?.id !== props.user.id,
},
{ {
id: 'copy-id', id: 'copy-id',
label: formatMessage(commonMessages.copyIdButton), label: formatMessage(commonMessages.copyIdButton),
@@ -1,5 +1,38 @@
<template> <template>
<template v-if="user"> <template v-if="user">
<NewModal
ref="blockUserModal"
:header="formatMessage(messages.blockUserTitle, { username: user.username })"
:closable="!isBlockingUser"
fade="danger"
max-width="500px"
>
<Admonition
type="critical"
:header="formatMessage(messages.blockUserAdmonitionTitle)"
>
{{ formatMessage(messages.blockUserAdmonitionBody, { username: user.username }) }}
</Admonition>
<template #actions>
<div class="flex justify-end gap-2">
<ButtonStyled type="outlined">
<button type="button" :disabled="isBlockingUser" @click="blockUserModal?.hide()">
<XIcon />
{{ formatMessage(commonMessages.cancelButton) }}
</button>
</ButtonStyled>
<ButtonStyled color="red">
<button type="button" :disabled="isBlockingUser" @click="confirmBlockUser">
<SpinnerIcon v-if="isBlockingUser" class="animate-spin" />
<BanIcon v-else />
{{ formatMessage(messages.blockButton) }}
</button>
</ButtonStyled>
</div>
</template>
</NewModal>
<NewModal <NewModal
v-if="variant === 'web'" v-if="variant === 'web'"
ref="editRoleModal" ref="editRoleModal"
@@ -151,10 +184,12 @@
:is-admin="isAdminViewing" :is-admin="isAdminViewing"
:is-staff="isStaffViewing" :is-staff="isStaffViewing"
:show-staff-actions="variant === 'web'" :show-staff-actions="variant === 'web'"
:is-blocked="isBlocked"
:projects-count="projects.length" :projects-count="projects.length"
:downloads="sumDownloads" :downloads="sumDownloads"
@manage-projects="openPath('/dashboard/projects')" @manage-projects="openPath('/dashboard/projects')"
@report="reportProfile" @report="reportProfile"
@block="handleBlockAction"
@copy-id="copyId" @copy-id="copyId"
@copy-permalink="copyPermalink" @copy-permalink="copyPermalink"
@open-billing="openPath(`/admin/billing/${user.id}`)" @open-billing="openPath(`/admin/billing/${user.id}`)"
@@ -392,6 +427,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Labrinth } from '@modrinth/api-client' import type { Labrinth } from '@modrinth/api-client'
import { import {
BanIcon,
BoxIcon, BoxIcon,
CheckIcon, CheckIcon,
GlobeIcon, GlobeIcon,
@@ -411,6 +447,7 @@ import { useQuery, useQueryClient } from '@tanstack/vue-query'
import { computed, ref, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import Admonition from '#ui/components/base/Admonition.vue'
import AutoLink from '#ui/components/base/AutoLink.vue' import AutoLink from '#ui/components/base/AutoLink.vue'
import Avatar from '#ui/components/base/Avatar.vue' import Avatar from '#ui/components/base/Avatar.vue'
import ButtonStyled from '#ui/components/base/ButtonStyled.vue' import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
@@ -429,7 +466,7 @@ import { defineMessages, useVIntl } from '#ui/composables'
import { injectAuth, injectNotificationManager, injectPageContext, injectTags } from '#ui/providers' import { injectAuth, injectNotificationManager, injectPageContext, injectTags } from '#ui/providers'
import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils' import { commonMessages, getProjectTypeTitleMessage } from '#ui/utils'
import { injectUserProfile } from './providers' import { blockedUsersQueryKey, injectUserProfile } from './providers'
import { import {
hasActivePride26Midas, hasActivePride26Midas,
hasPride26Badge, hasPride26Badge,
@@ -608,6 +645,55 @@ const messages = defineMessages({
id: 'profile.role.update-error-description', id: 'profile.role.update-error-description',
defaultMessage: 'An error occurred while updating the user role. Please try again.', defaultMessage: 'An error occurred while updating the user role. Please try again.',
}, },
blockButton: {
id: 'profile.button.block',
defaultMessage: 'Block',
},
unblockUserSuccessTitle: {
id: 'profile.unblock-user.success-title',
defaultMessage: 'User unblocked',
},
unblockUserSuccessDescription: {
id: 'profile.unblock-user.success-description',
defaultMessage: '{username} has been unblocked.',
},
unblockUserErrorTitle: {
id: 'profile.unblock-user.error-title',
defaultMessage: 'Failed to unblock user',
},
unblockUserErrorDescription: {
id: 'profile.unblock-user.error-description',
defaultMessage: 'An error occurred while unblocking this user. Please try again.',
},
blockUserTitle: {
id: 'profile.block-user.title',
defaultMessage: 'Block {username}',
},
blockUserAdmonitionTitle: {
id: 'profile.block-user.admonition-title',
defaultMessage: 'Are you sure you want to block this user?',
},
blockUserAdmonitionBody: {
id: 'profile.block-user.admonition-body',
defaultMessage:
'{username} will not be able to send you friend requests, invite you to shared instances or invite you to Modrinth Hosting servers.',
},
blockUserSuccessTitle: {
id: 'profile.block-user.success-title',
defaultMessage: 'User blocked',
},
blockUserSuccessDescription: {
id: 'profile.block-user.success-description',
defaultMessage: '{username} has been blocked.',
},
blockUserErrorTitle: {
id: 'profile.block-user.error-title',
defaultMessage: 'Failed to block user',
},
blockUserErrorDescription: {
id: 'profile.block-user.error-description',
defaultMessage: 'An error occurred while blocking this user. Please try again.',
},
}) })
const userQuery = useQuery({ const userQuery = useQuery({
@@ -634,6 +720,12 @@ const collectionsQuery = useQuery({
enabled: computed(() => Boolean(props.userId)), enabled: computed(() => Boolean(props.userId)),
staleTime: 30_000, staleTime: 30_000,
}) })
const blockedUsersQuery = useQuery({
queryKey: computed(() => blockedUsersQueryKey(auth.user.value?.id)),
queryFn: userProfile.getBlockedUsers,
enabled: computed(() => Boolean(auth.user.value)),
staleTime: 30_000,
})
const user = computed(() => userQuery.data.value) const user = computed(() => userQuery.data.value)
const projects = computed<ResolvedProject[]>(() => const projects = computed<ResolvedProject[]>(() =>
@@ -644,6 +736,9 @@ const projects = computed<ResolvedProject[]>(() =>
) )
const organizations = computed(() => organizationsQuery.data.value ?? []) const organizations = computed(() => organizationsQuery.data.value ?? [])
const collections = computed(() => collectionsQuery.data.value ?? []) const collections = computed(() => collectionsQuery.data.value ?? [])
const isBlocked = computed(() =>
user.value ? (blockedUsersQuery.data.value ?? []).includes(user.value.id) : false,
)
const selectedProjectType = computed(() => { const selectedProjectType = computed(() => {
const projectType = props.projectType const projectType = props.projectType
@@ -826,8 +921,11 @@ async function retryQueries(): Promise<void> {
const userDetailsModal = ref<ModalRef | null>(null) const userDetailsModal = ref<ModalRef | null>(null)
const editRoleModal = ref<ModalRef | null>(null) const editRoleModal = ref<ModalRef | null>(null)
const blockUserModal = ref<ModalRef | null>(null)
const selectedRole = ref<Labrinth.Users.v3.Role | null>(null) const selectedRole = ref<Labrinth.Users.v3.Role | null>(null)
const isSavingRole = ref(false) const isSavingRole = ref(false)
const isBlockingUser = ref(false)
const isUnblockingUser = ref(false)
const roleOptions = [ const roleOptions = [
{ value: 'developer', label: 'Developer' }, { value: 'developer', label: 'Developer' },
{ value: 'moderator', label: 'Moderator' }, { value: 'moderator', label: 'Moderator' },
@@ -851,6 +949,82 @@ function openRoleEditModal(): void {
editRoleModal.value?.show() editRoleModal.value?.show()
} }
async function handleBlockAction(): Promise<void> {
if (!auth.user.value) {
await auth.requestSignIn(route.fullPath)
return
}
if (isBlocked.value) {
await unblockCurrentUser()
return
}
blockUserModal.value?.show()
}
async function confirmBlockUser(): Promise<void> {
if (!user.value || isBlockingUser.value) return
const blockedUser = user.value
const authUserId = auth.user.value?.id
isBlockingUser.value = true
try {
await userProfile.blockUser(blockedUser.id)
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
blockedUsersQueryKey(authUserId),
(blockedUsers = []) =>
blockedUsers.includes(blockedUser.id) ? blockedUsers : [...blockedUsers, blockedUser.id],
)
blockUserModal.value?.hide()
notificationManager.addNotification({
type: 'success',
title: formatMessage(messages.blockUserSuccessTitle),
text: formatMessage(messages.blockUserSuccessDescription, {
username: blockedUser.username,
}),
})
} catch {
notificationManager.addNotification({
type: 'error',
title: formatMessage(messages.blockUserErrorTitle),
text: formatMessage(messages.blockUserErrorDescription),
})
} finally {
isBlockingUser.value = false
}
}
async function unblockCurrentUser(): Promise<void> {
if (!user.value || isUnblockingUser.value) return
const blockedUser = user.value
const authUserId = auth.user.value?.id
isUnblockingUser.value = true
try {
await userProfile.unblockUser(blockedUser.id)
queryClient.setQueryData<Labrinth.BlockedUsers.v3.BlockedUserId[]>(
blockedUsersQueryKey(authUserId),
(blockedUsers = []) => blockedUsers.filter((userId) => userId !== blockedUser.id),
)
notificationManager.addNotification({
type: 'success',
title: formatMessage(messages.unblockUserSuccessTitle),
text: formatMessage(messages.unblockUserSuccessDescription, {
username: blockedUser.username,
}),
})
} catch {
notificationManager.addNotification({
type: 'error',
title: formatMessage(messages.unblockUserErrorTitle),
text: formatMessage(messages.unblockUserErrorDescription),
})
} finally {
isUnblockingUser.value = false
}
}
function cancelRoleEdit(): void { function cancelRoleEdit(): void {
selectedRole.value = user.value?.role ?? null selectedRole.value = user.value?.role ?? null
editRoleModal.value?.hide() editRoleModal.value?.hide()
@@ -11,8 +11,14 @@ export interface UserProfileContext {
userId: string, userId: string,
patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>, patch: Partial<Pick<Labrinth.Users.v3.User, 'badges' | 'role'>>,
) => Promise<void> ) => Promise<void>
getBlockedUsers: () => Promise<Labrinth.BlockedUsers.v3.BlockedUserId[]>
blockUser: (userId: string) => Promise<void>
unblockUser: (userId: string) => Promise<void>
} }
export const blockedUsersQueryKey = (userId?: string | null) =>
['blocked-users', userId ?? null] as const
export const [injectUserProfile, provideUserProfile] = createContext<UserProfileContext>( export const [injectUserProfile, provideUserProfile] = createContext<UserProfileContext>(
'UserProfilePageLayout', 'UserProfilePageLayout',
'userProfileContext', 'userProfileContext',