From 7d00772ee61fe4c1e4984dd38c55259b49c0fc91 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Tue, 27 Feb 2024 11:23:18 +0100 Subject: [PATCH] Implement the user profile edit modal --- frontend/locales/en.json | 20 +- frontend/package-lock.json | 8 +- frontend/package.json | 2 +- .../UnverifiedEmailAlert.tsx | 2 +- frontend/src/components/UserGreeting.tsx | 62 --- .../UserGreeting.module.css | 38 +- .../UserGreeting/UserGreeting.stories.tsx | 93 ++++ .../components/UserGreeting/UserGreeting.tsx | 207 ++++++++ frontend/src/components/UserGreeting/index.ts | 15 + .../UserProfile/UserName.module.css | 14 - .../src/components/UserProfile/UserName.tsx | 149 ------ frontend/src/gql/gql.ts | 50 +- frontend/src/gql/graphql.ts | 450 ++++++++---------- frontend/src/routes/_account.index.tsx | 12 +- frontend/src/routes/_account.tsx | 55 ++- 15 files changed, 622 insertions(+), 555 deletions(-) delete mode 100644 frontend/src/components/UserGreeting.tsx rename frontend/src/components/{ => UserGreeting}/UserGreeting.module.css (56%) create mode 100644 frontend/src/components/UserGreeting/UserGreeting.stories.tsx create mode 100644 frontend/src/components/UserGreeting/UserGreeting.tsx create mode 100644 frontend/src/components/UserGreeting/index.ts delete mode 100644 frontend/src/components/UserProfile/UserName.module.css delete mode 100644 frontend/src/components/UserProfile/UserName.tsx diff --git a/frontend/locales/en.json b/frontend/locales/en.json index 89b16c33d..60d77f8f0 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -2,8 +2,10 @@ "action": { "back": "Back", "cancel": "Cancel", + "clear": "Clear", "close": "Close", "continue": "Continue", + "edit": "Edit", "save": "Save" }, "branding": { @@ -17,12 +19,20 @@ } }, "common": { - "error": "Error", "loading": "Loading…", "next": "Next", "previous": "Previous" }, "frontend": { + "account": { + "edit_profile": { + "display_name_help": "This is what others will see wherever you’re signed in.", + "display_name_label": "Display name", + "title": "Edit profile", + "username_label": "Username" + }, + "title": "Your account" + }, "add_email_form": { "email_denied_alert": { "text": "The entered email is not allowed by the server policy.", @@ -76,8 +86,8 @@ "inactive_90_days": "Inactive for 90+ days" }, "nav": { - "profile": "Profile", - "sessions": "Sessions" + "devices": "Devices", + "settings": "Settings" }, "not_found_alert_title": "Not found.", "not_logged_in_alert": "You're not logged in.", @@ -155,12 +165,8 @@ "retry_button": "Resend code" }, "user_email_list": { - "heading": "Emails", "no_primary_email_alert": "No primary email address" }, - "user_name": { - "display_name_field_label": "Display Name" - }, "user_sessions_overview": { "active_sessions:one": "{{count}} active session", "active_sessions:other": "{{count}} active sessions", diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6c55e2193..b194ab64c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -21,7 +21,7 @@ "@urql/exchange-refocus": "^1.0.2", "@urql/exchange-request-policy": "^1.0.2", "@vector-im/compound-design-tokens": "1.1.1", - "@vector-im/compound-web": "^3.1.2", + "@vector-im/compound-web": "^3.1.3", "classnames": "^2.5.1", "date-fns": "^3.3.1", "graphql": "^16.8.1", @@ -9330,9 +9330,9 @@ } }, "node_modules/@vector-im/compound-web": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-3.1.2.tgz", - "integrity": "sha512-JTKnGBO0wHPOdcHvqc1tG/KQ6+OSDXgmjQnT0bNvu+FH3wVdFJ+t1PB9mlV/SFunxqNWrDZyPVzX3BGkoNSLtw==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@vector-im/compound-web/-/compound-web-3.1.3.tgz", + "integrity": "sha512-h1uEKxMrZXUlEA2b8sd57WbxDy9LV8E0MYbz1vdKbU0n3lJb8neUbCAJE7PdQUoOSCi91jw8H+xH8XRLxTYYYw==", "dependencies": { "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dropdown-menu": "^2.0.6", diff --git a/frontend/package.json b/frontend/package.json index 4620afacb..245deb6ba 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -29,7 +29,7 @@ "@urql/exchange-refocus": "^1.0.2", "@urql/exchange-request-policy": "^1.0.2", "@vector-im/compound-design-tokens": "1.1.1", - "@vector-im/compound-web": "^3.1.2", + "@vector-im/compound-web": "^3.1.3", "classnames": "^2.5.1", "date-fns": "^3.3.1", "graphql": "^16.8.1", diff --git a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx index 35d377972..43f3700fd 100644 --- a/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx +++ b/frontend/src/components/UnverifiedEmailAlert/UnverifiedEmailAlert.tsx @@ -22,7 +22,7 @@ import { Link } from "../Link"; import styles from "./UnverifiedEmailAlert.module.css"; export const UNVERIFIED_EMAILS_FRAGMENT = graphql(/* GraphQL */ ` - fragment UnverifiedEmailAlert on User { + fragment UnverifiedEmailAlert_user on User { id unverifiedEmails: emails(first: 0, state: PENDING) { totalCount diff --git a/frontend/src/components/UserGreeting.tsx b/frontend/src/components/UserGreeting.tsx deleted file mode 100644 index 23ee05dca..000000000 --- a/frontend/src/components/UserGreeting.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Heading, Text, Avatar } from "@vector-im/compound-web"; - -import { FragmentType, graphql, useFragment } from "../gql"; - -import UnverifiedEmailAlert from "./UnverifiedEmailAlert"; -import styles from "./UserGreeting.module.css"; - -export const USER_GREETING_FRAGMENT = graphql(/* GraphQL */ ` - fragment UserGreeting_user on User { - id - username - matrix { - mxid - displayName - } - - ...UnverifiedEmailAlert - } -`); - -type Props = { - user: FragmentType; -}; - -const UserGreeting: React.FC = ({ user }) => { - const data = useFragment(USER_GREETING_FRAGMENT, user); - - return ( - <> -
- - - {data.matrix.displayName || data.username} - - - {data.matrix.mxid} - -
- - - ); -}; - -export default UserGreeting; diff --git a/frontend/src/components/UserGreeting.module.css b/frontend/src/components/UserGreeting/UserGreeting.module.css similarity index 56% rename from frontend/src/components/UserGreeting.module.css rename to frontend/src/components/UserGreeting/UserGreeting.module.css index 2cbd3c928..c7009199a 100644 --- a/frontend/src/components/UserGreeting.module.css +++ b/frontend/src/components/UserGreeting/UserGreeting.module.css @@ -13,14 +13,44 @@ * limitations under the License. */ -.header { +.user { display: flex; - flex-direction: column; + flex-direction: row; align-items: center; gap: var(--cpd-space-4x); - text-align: center; + outline: 1px solid var(--cpd-color-gray-400); + outline-offset: -1px; + border-radius: var(--cpd-space-3x); + padding: var(--cpd-space-4x) +} + +.meta { + flex: 1 1; + + /* Make sure the text properly truncates */ + min-width: 0; + + & p { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } +} + +.edit-button { + align-self: flex-start; } .mxid { color: var(--cpd-color-text-secondary); -} \ No newline at end of file +} + +.dialog-form { + display: flex; + flex-direction: column; + gap: var(--cpd-space-4x); + align-self: center; + max-width: 378px; + width: 100%; + margin-block-end: var(--cpd-space-9x); +} diff --git a/frontend/src/components/UserGreeting/UserGreeting.stories.tsx b/frontend/src/components/UserGreeting/UserGreeting.stories.tsx new file mode 100644 index 000000000..fe50a6da4 --- /dev/null +++ b/frontend/src/components/UserGreeting/UserGreeting.stories.tsx @@ -0,0 +1,93 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Meta, StoryObj } from "@storybook/react"; +import { Provider } from "urql"; +import { fromValue, delay, pipe } from "wonka"; + +import { makeFragmentData } from "../../gql"; +import { + SetDisplayNameMutation, + SetDisplayNameStatus, +} from "../../gql/graphql"; + +import UserGreeting, { FRAGMENT } from "./UserGreeting"; + +const Template: React.FC<{ + displayName?: string; + mxid: string; +}> = ({ displayName, mxid }) => { + const userId = "user id"; + + const mockClient = { + /* This will resolve after a small delay */ + executeMutation: () => + pipe( + fromValue({ + data: { + setDisplayName: { + status: SetDisplayNameStatus.Set, + user: { id: userId, matrix: { displayName } }, + }, + }, + } satisfies { data: SetDisplayNameMutation }), + delay(300), + ), + }; + + const user = makeFragmentData( + { + id: "user id", + matrix: { + mxid, + displayName, + }, + }, + FRAGMENT, + ); + + return ( + + + + ); +}; + +const meta = { + title: "UI/User Greeting", + component: Template, + args: { + displayName: "Kilgore Trout", + mxid: "@kilgore:matrix.org", + }, + argTypes: { + displayName: { + control: "text", + }, + mxid: { + control: "text", + }, + }, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = {}; + +export const NoDisplayName: Story = { + args: { + displayName: undefined, + }, +}; diff --git a/frontend/src/components/UserGreeting/UserGreeting.tsx b/frontend/src/components/UserGreeting/UserGreeting.tsx new file mode 100644 index 000000000..2abb3b625 --- /dev/null +++ b/frontend/src/components/UserGreeting/UserGreeting.tsx @@ -0,0 +1,207 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import IconClose from "@vector-im/compound-design-tokens/icons/close.svg?react"; +import IconEdit from "@vector-im/compound-design-tokens/icons/edit.svg?react"; +import { + Text, + Avatar, + IconButton, + Tooltip, + Button, + Form, +} from "@vector-im/compound-web"; +import { ComponentPropsWithoutRef, forwardRef, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useMutation } from "urql"; + +import { FragmentType, graphql, useFragment } from "../../gql"; +import { SetDisplayNameStatus } from "../../gql/graphql"; +import * as Dialog from "../Dialog"; +import LoadingSpinner from "../LoadingSpinner"; + +import styles from "./UserGreeting.module.css"; + +export const FRAGMENT = graphql(/* GraphQL */ ` + fragment UserGreeting_user on User { + id + matrix { + mxid + displayName + } + } +`); + +const SET_DISPLAYNAME_MUTATION = graphql(/* GraphQL */ ` + mutation SetDisplayName($userId: ID!, $displayName: String) { + setDisplayName(input: { userId: $userId, displayName: $displayName }) { + status + user { + id + matrix { + displayName + } + } + } + } +`); + +// This needs to be its own component because else props and refs aren't passed properly in the trigger +const EditButton = forwardRef< + HTMLButtonElement, + { label: string } & ComponentPropsWithoutRef<"button"> +>(({ label, ...props }, ref) => ( + + + + + +)); + +type Props = { + user: FragmentType; +}; + +const UserGreeting: React.FC = ({ user }) => { + const fieldRef = useRef(null); + const data = useFragment(FRAGMENT, user); + + const [setDisplayNameResult, setDisplayName] = useMutation( + SET_DISPLAYNAME_MUTATION, + ); + + const [open, setOpen] = useState(false); + const { t } = useTranslation(); + + const onSubmit = async ( + event: React.FormEvent, + ): Promise => { + event.preventDefault(); + + const form = event.currentTarget; + const formData = new FormData(form); + const displayName = (formData.get("displayname") as string) || null; + + const result = await setDisplayName({ displayName, userId: data.id }); + + if (result.data?.setDisplayName.status === SetDisplayNameStatus.Set) { + setOpen(false); + } + }; + + return ( +
+ +
+ {data.matrix.displayName ? ( + <> + + {data.matrix.displayName} + + + {data.matrix.mxid} + + + ) : ( + + {data.matrix.mxid} + + )} +
+ + } + open={open} + onOpenChange={(open) => { + // Reset the form when the dialog is opened or closed + fieldRef.current?.form?.reset(); + setOpen(open); + }} + > + {t("frontend.account.edit_profile.title")} + + + + +
+ + + {t("frontend.account.edit_profile.display_name_label")} + + + { + if (fieldRef.current) { + fieldRef.current.value = ""; + fieldRef.current.focus(); + } + }} + /> + + + {t("frontend.account.edit_profile.display_name_help")} + + + + + + {t("frontend.account.edit_profile.username_label")} + + + +
+ + + {setDisplayNameResult.fetching && } + {t("action.save")} + +
+ + + + +
+
+ ); +}; + +export default UserGreeting; diff --git a/frontend/src/components/UserGreeting/index.ts b/frontend/src/components/UserGreeting/index.ts new file mode 100644 index 000000000..2f1e9ba6a --- /dev/null +++ b/frontend/src/components/UserGreeting/index.ts @@ -0,0 +1,15 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { default } from "./UserGreeting"; diff --git a/frontend/src/components/UserProfile/UserName.module.css b/frontend/src/components/UserProfile/UserName.module.css deleted file mode 100644 index c6ae3eeb5..000000000 --- a/frontend/src/components/UserProfile/UserName.module.css +++ /dev/null @@ -1,14 +0,0 @@ -/* Copyright 2023 The Matrix.org Foundation C.I.C. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ diff --git a/frontend/src/components/UserProfile/UserName.tsx b/frontend/src/components/UserProfile/UserName.tsx deleted file mode 100644 index b4ab8e361..000000000 --- a/frontend/src/components/UserProfile/UserName.tsx +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2023 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Alert, Button, Form } from "@vector-im/compound-web"; -import { useState, ChangeEventHandler } from "react"; -import { useTranslation } from "react-i18next"; -import { useMutation } from "urql"; - -import { FragmentType, graphql, useFragment } from "../../gql"; -import LoadingSpinner from "../LoadingSpinner/LoadingSpinner"; - -import styles from "./UserName.module.css"; - -const FRAGMENT = graphql(/* GraphQL */ ` - fragment UserName_user on User { - id - matrix { - displayName - } - } -`); - -const SET_DISPLAYNAME_MUTATION = graphql(/* GraphQL */ ` - mutation SetDisplayName($userId: ID!, $displayName: String) { - setDisplayName(input: { userId: $userId, displayName: $displayName }) { - status - user { - id - matrix { - displayName - } - } - } - } -`); - -const getErrorMessage = (result: { - error?: unknown; - data?: { setDisplayName: { status: string } }; -}): string | undefined => { - if (result.error) { - return "Failed to save display name. Please try again."; - } - if (result.data?.setDisplayName.status === "INVALID") { - return "Failed to save invalid display name."; - } -}; - -const UserName: React.FC<{ user: FragmentType }> = ({ - user, -}) => { - const data = useFragment(FRAGMENT, user); - const displayName = data.matrix.displayName || ""; - - const [setDisplayNameResult, setDisplayName] = useMutation( - SET_DISPLAYNAME_MUTATION, - ); - - const [hasChanges, setHasChanges] = useState(false); - - const { t } = useTranslation(); - - const onChange: ChangeEventHandler = (event): void => { - setHasChanges(event.target.value !== displayName); - }; - - const onSubmit = (event: React.FormEvent): void => { - event.preventDefault(); - - const form = event.currentTarget; - const formData = new FormData(form); - let newDisplayName = formData.get("displayname") as string | null; - - // set null to remove an existing username - if (newDisplayName === "") { - newDisplayName = null; - } - - // do nothing if no change - if ((!newDisplayName && !displayName) || newDisplayName === displayName) { - return; - } - - setDisplayName({ displayName: newDisplayName, userId: data.id }).then( - (result) => { - if (!result.data) { - console.error("Failed to set display name", result.error); - } else if (result.data.setDisplayName.status === "SET") { - // This should update the cache - } else if (result.data.setDisplayName.status === "INVALID") { - // reset to current saved display name - form.reset(); - } - - setHasChanges(false); - }, - ); - }; - - const errorMessage = getErrorMessage(setDisplayNameResult); - - return ( - - - - {t("frontend.user_name.display_name_field_label")} - - - - {!setDisplayNameResult.fetching && errorMessage && ( - - {errorMessage} - - )} - - - - ); -}; - -export default UserName; diff --git a/frontend/src/gql/gql.ts b/frontend/src/gql/gql.ts index 10745a36e..833f10e5a 100644 --- a/frontend/src/gql/gql.ts +++ b/frontend/src/gql/gql.ts @@ -33,26 +33,24 @@ const documents = { types.CompatSession_DetailFragmentDoc, "\n fragment OAuth2Session_detail on Oauth2Session {\n id\n scope\n createdAt\n finishedAt\n lastActiveIp\n lastActiveAt\n client {\n id\n clientId\n clientName\n clientUri\n logoUri\n }\n }\n": types.OAuth2Session_DetailFragmentDoc, - "\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n": - types.UnverifiedEmailAlertFragmentDoc, + "\n fragment UnverifiedEmailAlert_user on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n": + types.UnverifiedEmailAlert_UserFragmentDoc, "\n fragment UserEmail_email on UserEmail {\n id\n email\n confirmedAt\n }\n": types.UserEmail_EmailFragmentDoc, "\n mutation RemoveEmail($id: ID!) {\n removeEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n }\n }\n }\n": types.RemoveEmailDocument, "\n mutation SetPrimaryEmail($id: ID!) {\n setPrimaryEmail(input: { userEmailId: $id }) {\n status\n user {\n id\n primaryEmail {\n id\n }\n }\n }\n }\n": types.SetPrimaryEmailDocument, - "\n fragment UserGreeting_user on User {\n id\n username\n matrix {\n mxid\n displayName\n }\n\n ...UnverifiedEmailAlert\n }\n": + "\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n": types.UserGreeting_UserFragmentDoc, + "\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n": + types.SetDisplayNameDocument, "\n mutation AddEmail($userId: ID!, $email: String!) {\n addEmail(input: { userId: $userId, email: $email }) {\n status\n violations\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.AddEmailDocument, "\n query UserEmailListQuery(\n $userId: ID!\n $first: Int\n $after: String\n $last: Int\n $before: String\n ) {\n user(id: $userId) {\n id\n\n emails(first: $first, after: $after, last: $last, before: $before) {\n edges {\n cursor\n node {\n id\n ...UserEmail_email\n }\n }\n totalCount\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n": types.UserEmailListQueryDocument, "\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n": types.UserEmailList_UserFragmentDoc, - "\n fragment UserName_user on User {\n id\n matrix {\n displayName\n }\n }\n": - types.UserName_UserFragmentDoc, - "\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n": - types.SetDisplayNameDocument, "\n fragment BrowserSessionsOverview_user on User {\n id\n\n browserSessions(first: 0, state: ACTIVE) {\n totalCount\n }\n }\n": types.BrowserSessionsOverview_UserFragmentDoc, "\n fragment UserEmail_verifyEmail on UserEmail {\n id\n email\n }\n": @@ -61,7 +59,7 @@ const documents = { types.VerifyEmailDocument, "\n mutation ResendVerificationEmail($id: ID!) {\n sendVerificationEmail(input: { userEmailId: $id }) {\n status\n\n user {\n id\n primaryEmail {\n id\n }\n }\n\n email {\n id\n ...UserEmail_email\n }\n }\n }\n": types.ResendVerificationEmailDocument, - "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserName_user\n ...UserEmailList_user\n }\n }\n }\n": + "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserEmailList_user\n }\n }\n }\n": types.UserProfileQueryDocument, "\n query SessionDetailQuery($id: ID!) {\n viewerSession {\n ... on Node {\n id\n }\n }\n\n node(id: $id) {\n __typename\n id\n ...CompatSession_detail\n ...OAuth2Session_detail\n ...BrowserSession_detail\n }\n }\n": types.SessionDetailQueryDocument, @@ -71,7 +69,7 @@ const documents = { types.SessionsOverviewQueryDocument, "\n query AppSessionsListQuery(\n $before: String\n $after: String\n $first: Int\n $last: Int\n ) {\n viewer {\n __typename\n\n ... on User {\n id\n appSessions(\n before: $before\n after: $after\n first: $first\n last: $last\n state: ACTIVE\n ) {\n edges {\n cursor\n node {\n __typename\n ...CompatSession_session\n ...OAuth2Session_session\n }\n }\n\n totalCount\n pageInfo {\n startCursor\n endCursor\n hasNextPage\n hasPreviousPage\n }\n }\n }\n }\n }\n": types.AppSessionsListQueryDocument, - "\n query CurrentUserGreeting {\n viewer {\n __typename\n ...UserGreeting_user\n }\n }\n": + "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n id\n ...UnverifiedEmailAlert_user\n ...UserGreeting_user\n }\n }\n }\n }\n": types.CurrentUserGreetingDocument, "\n query OAuth2ClientQuery($id: ID!) {\n oauth2Client(id: $id) {\n ...OAuth2Client_detail\n }\n }\n": types.OAuth2ClientQueryDocument, @@ -163,8 +161,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n", -): (typeof documents)["\n fragment UnverifiedEmailAlert on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n"]; + source: "\n fragment UnverifiedEmailAlert_user on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n", +): (typeof documents)["\n fragment UnverifiedEmailAlert_user on User {\n id\n unverifiedEmails: emails(first: 0, state: PENDING) {\n totalCount\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -187,8 +185,14 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n fragment UserGreeting_user on User {\n id\n username\n matrix {\n mxid\n displayName\n }\n\n ...UnverifiedEmailAlert\n }\n", -): (typeof documents)["\n fragment UserGreeting_user on User {\n id\n username\n matrix {\n mxid\n displayName\n }\n\n ...UnverifiedEmailAlert\n }\n"]; + source: "\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n", +): (typeof documents)["\n fragment UserGreeting_user on User {\n id\n matrix {\n mxid\n displayName\n }\n }\n"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql( + source: "\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n", +): (typeof documents)["\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -207,18 +211,6 @@ export function graphql( export function graphql( source: "\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n", ): (typeof documents)["\n fragment UserEmailList_user on User {\n id\n primaryEmail {\n id\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: "\n fragment UserName_user on User {\n id\n matrix {\n displayName\n }\n }\n", -): (typeof documents)["\n fragment UserName_user on User {\n id\n matrix {\n displayName\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql( - source: "\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n", -): (typeof documents)["\n mutation SetDisplayName($userId: ID!, $displayName: String) {\n setDisplayName(input: { userId: $userId, displayName: $displayName }) {\n status\n user {\n id\n matrix {\n displayName\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -247,8 +239,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserName_user\n ...UserEmailList_user\n }\n }\n }\n", -): (typeof documents)["\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserName_user\n ...UserEmailList_user\n }\n }\n }\n"]; + source: "\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserEmailList_user\n }\n }\n }\n", +): (typeof documents)["\n query UserProfileQuery {\n viewer {\n __typename\n ... on User {\n id\n ...UserEmailList_user\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -277,8 +269,8 @@ export function graphql( * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql( - source: "\n query CurrentUserGreeting {\n viewer {\n __typename\n ...UserGreeting_user\n }\n }\n", -): (typeof documents)["\n query CurrentUserGreeting {\n viewer {\n __typename\n ...UserGreeting_user\n }\n }\n"]; + source: "\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n id\n ...UnverifiedEmailAlert_user\n ...UserGreeting_user\n }\n }\n }\n }\n", +): (typeof documents)["\n query CurrentUserGreeting {\n viewerSession {\n __typename\n\n ... on BrowserSession {\n id\n\n user {\n id\n ...UnverifiedEmailAlert_user\n ...UserGreeting_user\n }\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index b81e273c4..7660fd0ba 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1371,11 +1371,11 @@ export type OAuth2Session_DetailFragment = { }; } & { " $fragmentName"?: "OAuth2Session_DetailFragment" }; -export type UnverifiedEmailAlertFragment = { +export type UnverifiedEmailAlert_UserFragment = { __typename?: "User"; id: string; unverifiedEmails: { __typename?: "UserEmailConnection"; totalCount: number }; -} & { " $fragmentName"?: "UnverifiedEmailAlertFragment" }; +} & { " $fragmentName"?: "UnverifiedEmailAlert_UserFragment" }; export type UserEmail_EmailFragment = { __typename?: "UserEmail"; @@ -1414,20 +1414,33 @@ export type SetPrimaryEmailMutation = { }; }; -export type UserGreeting_UserFragment = ({ +export type UserGreeting_UserFragment = { __typename?: "User"; id: string; - username: string; matrix: { __typename?: "MatrixUser"; mxid: string; displayName?: string | null; }; -} & { - " $fragmentRefs"?: { - UnverifiedEmailAlertFragment: UnverifiedEmailAlertFragment; +} & { " $fragmentName"?: "UserGreeting_UserFragment" }; + +export type SetDisplayNameMutationVariables = Exact<{ + userId: Scalars["ID"]["input"]; + displayName?: InputMaybe; +}>; + +export type SetDisplayNameMutation = { + __typename?: "Mutation"; + setDisplayName: { + __typename?: "SetDisplayNamePayload"; + status: SetDisplayNameStatus; + user?: { + __typename?: "User"; + id: string; + matrix: { __typename?: "MatrixUser"; displayName?: string | null }; + } | null; }; -}) & { " $fragmentName"?: "UserGreeting_UserFragment" }; +}; export type AddEmailMutationVariables = Exact<{ userId: Scalars["ID"]["input"]; @@ -1492,30 +1505,6 @@ export type UserEmailList_UserFragment = { primaryEmail?: { __typename?: "UserEmail"; id: string } | null; } & { " $fragmentName"?: "UserEmailList_UserFragment" }; -export type UserName_UserFragment = { - __typename?: "User"; - id: string; - matrix: { __typename?: "MatrixUser"; displayName?: string | null }; -} & { " $fragmentName"?: "UserName_UserFragment" }; - -export type SetDisplayNameMutationVariables = Exact<{ - userId: Scalars["ID"]["input"]; - displayName?: InputMaybe; -}>; - -export type SetDisplayNameMutation = { - __typename?: "Mutation"; - setDisplayName: { - __typename?: "SetDisplayNamePayload"; - status: SetDisplayNameStatus; - user?: { - __typename?: "User"; - id: string; - matrix: { __typename?: "MatrixUser"; displayName?: string | null }; - } | null; - }; -}; - export type BrowserSessionsOverview_UserFragment = { __typename?: "User"; id: string; @@ -1584,7 +1573,6 @@ export type UserProfileQueryQuery = { | { __typename: "Anonymous" } | ({ __typename: "User"; id: string } & { " $fragmentRefs"?: { - UserName_UserFragment: UserName_UserFragment; UserEmailList_UserFragment: UserEmailList_UserFragment; }; }); @@ -1731,13 +1719,19 @@ export type CurrentUserGreetingQueryVariables = Exact<{ [key: string]: never }>; export type CurrentUserGreetingQuery = { __typename?: "Query"; - viewer: + viewerSession: | { __typename: "Anonymous" } - | ({ __typename: "User" } & { - " $fragmentRefs"?: { - UserGreeting_UserFragment: UserGreeting_UserFragment; + | { + __typename: "BrowserSession"; + id: string; + user: { __typename?: "User"; id: string } & { + " $fragmentRefs"?: { + UnverifiedEmailAlert_UserFragment: UnverifiedEmailAlert_UserFragment; + UserGreeting_UserFragment: UserGreeting_UserFragment; + }; }; - }); + } + | { __typename: "Oauth2Session" }; }; export type OAuth2ClientQueryQueryVariables = Exact<{ @@ -2123,33 +2117,12 @@ export const OAuth2Session_DetailFragmentDoc = { }, ], } as unknown as DocumentNode; -export const UserEmail_EmailFragmentDoc = { +export const UnverifiedEmailAlert_UserFragmentDoc = { kind: "Document", definitions: [ { kind: "FragmentDefinition", - name: { kind: "Name", value: "UserEmail_email" }, - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "UserEmail" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { kind: "Field", name: { kind: "Name", value: "email" } }, - { kind: "Field", name: { kind: "Name", value: "confirmedAt" } }, - ], - }, - }, - ], -} as unknown as DocumentNode; -export const UnverifiedEmailAlertFragmentDoc = { - kind: "Document", - definitions: [ - { - kind: "FragmentDefinition", - name: { kind: "Name", value: "UnverifiedEmailAlert" }, + name: { kind: "Name", value: "UnverifiedEmailAlert_user" }, typeCondition: { kind: "NamedType", name: { kind: "Name", value: "User" }, @@ -2185,43 +2158,34 @@ export const UnverifiedEmailAlertFragmentDoc = { }, }, ], -} as unknown as DocumentNode; -export const UserGreeting_UserFragmentDoc = { +} as unknown as DocumentNode; +export const UserEmail_EmailFragmentDoc = { kind: "Document", definitions: [ { kind: "FragmentDefinition", - name: { kind: "Name", value: "UserGreeting_user" }, + name: { kind: "Name", value: "UserEmail_email" }, typeCondition: { kind: "NamedType", - name: { kind: "Name", value: "User" }, + name: { kind: "Name", value: "UserEmail" }, }, selectionSet: { kind: "SelectionSet", selections: [ { kind: "Field", name: { kind: "Name", value: "id" } }, - { kind: "Field", name: { kind: "Name", value: "username" } }, - { - kind: "Field", - name: { kind: "Name", value: "matrix" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "mxid" } }, - { kind: "Field", name: { kind: "Name", value: "displayName" } }, - ], - }, - }, - { - kind: "FragmentSpread", - name: { kind: "Name", value: "UnverifiedEmailAlert" }, - }, + { kind: "Field", name: { kind: "Name", value: "email" } }, + { kind: "Field", name: { kind: "Name", value: "confirmedAt" } }, ], }, }, + ], +} as unknown as DocumentNode; +export const UserGreeting_UserFragmentDoc = { + kind: "Document", + definitions: [ { kind: "FragmentDefinition", - name: { kind: "Name", value: "UnverifiedEmailAlert" }, + name: { kind: "Name", value: "UserGreeting_user" }, typeCondition: { kind: "NamedType", name: { kind: "Name", value: "User" }, @@ -2232,24 +2196,12 @@ export const UserGreeting_UserFragmentDoc = { { kind: "Field", name: { kind: "Name", value: "id" } }, { kind: "Field", - alias: { kind: "Name", value: "unverifiedEmails" }, - name: { kind: "Name", value: "emails" }, - arguments: [ - { - kind: "Argument", - name: { kind: "Name", value: "first" }, - value: { kind: "IntValue", value: "0" }, - }, - { - kind: "Argument", - name: { kind: "Name", value: "state" }, - value: { kind: "EnumValue", value: "PENDING" }, - }, - ], + name: { kind: "Name", value: "matrix" }, selectionSet: { kind: "SelectionSet", selections: [ - { kind: "Field", name: { kind: "Name", value: "totalCount" } }, + { kind: "Field", name: { kind: "Name", value: "mxid" } }, + { kind: "Field", name: { kind: "Name", value: "displayName" } }, ], }, }, @@ -2287,35 +2239,6 @@ export const UserEmailList_UserFragmentDoc = { }, ], } as unknown as DocumentNode; -export const UserName_UserFragmentDoc = { - kind: "Document", - definitions: [ - { - kind: "FragmentDefinition", - name: { kind: "Name", value: "UserName_user" }, - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "User" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { - kind: "Field", - name: { kind: "Name", value: "matrix" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "displayName" } }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode; export const BrowserSessionsOverview_UserFragmentDoc = { kind: "Document", definitions: [ @@ -2826,6 +2749,105 @@ export const SetPrimaryEmailDocument = { SetPrimaryEmailMutation, SetPrimaryEmailMutationVariables >; +export const SetDisplayNameDocument = { + kind: "Document", + definitions: [ + { + kind: "OperationDefinition", + operation: "mutation", + name: { kind: "Name", value: "SetDisplayName" }, + variableDefinitions: [ + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "userId" }, + }, + type: { + kind: "NonNullType", + type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }, + }, + }, + { + kind: "VariableDefinition", + variable: { + kind: "Variable", + name: { kind: "Name", value: "displayName" }, + }, + type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "setDisplayName" }, + arguments: [ + { + kind: "Argument", + name: { kind: "Name", value: "input" }, + value: { + kind: "ObjectValue", + fields: [ + { + kind: "ObjectField", + name: { kind: "Name", value: "userId" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "userId" }, + }, + }, + { + kind: "ObjectField", + name: { kind: "Name", value: "displayName" }, + value: { + kind: "Variable", + name: { kind: "Name", value: "displayName" }, + }, + }, + ], + }, + }, + ], + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "status" } }, + { + kind: "Field", + name: { kind: "Name", value: "user" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "Field", + name: { kind: "Name", value: "matrix" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "displayName" }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], +} as unknown as DocumentNode< + SetDisplayNameMutation, + SetDisplayNameMutationVariables +>; export const AddEmailDocument = { kind: "Document", definitions: [ @@ -3137,105 +3159,6 @@ export const UserEmailListQueryDocument = { UserEmailListQueryQuery, UserEmailListQueryQueryVariables >; -export const SetDisplayNameDocument = { - kind: "Document", - definitions: [ - { - kind: "OperationDefinition", - operation: "mutation", - name: { kind: "Name", value: "SetDisplayName" }, - variableDefinitions: [ - { - kind: "VariableDefinition", - variable: { - kind: "Variable", - name: { kind: "Name", value: "userId" }, - }, - type: { - kind: "NonNullType", - type: { kind: "NamedType", name: { kind: "Name", value: "ID" } }, - }, - }, - { - kind: "VariableDefinition", - variable: { - kind: "Variable", - name: { kind: "Name", value: "displayName" }, - }, - type: { kind: "NamedType", name: { kind: "Name", value: "String" } }, - }, - ], - selectionSet: { - kind: "SelectionSet", - selections: [ - { - kind: "Field", - name: { kind: "Name", value: "setDisplayName" }, - arguments: [ - { - kind: "Argument", - name: { kind: "Name", value: "input" }, - value: { - kind: "ObjectValue", - fields: [ - { - kind: "ObjectField", - name: { kind: "Name", value: "userId" }, - value: { - kind: "Variable", - name: { kind: "Name", value: "userId" }, - }, - }, - { - kind: "ObjectField", - name: { kind: "Name", value: "displayName" }, - value: { - kind: "Variable", - name: { kind: "Name", value: "displayName" }, - }, - }, - ], - }, - }, - ], - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "status" } }, - { - kind: "Field", - name: { kind: "Name", value: "user" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { - kind: "Field", - name: { kind: "Name", value: "matrix" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { - kind: "Field", - name: { kind: "Name", value: "displayName" }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], - }, - }, - ], -} as unknown as DocumentNode< - SetDisplayNameMutation, - SetDisplayNameMutationVariables ->; export const VerifyEmailDocument = { kind: "Document", definitions: [ @@ -3499,10 +3422,6 @@ export const UserProfileQueryDocument = { kind: "SelectionSet", selections: [ { kind: "Field", name: { kind: "Name", value: "id" } }, - { - kind: "FragmentSpread", - name: { kind: "Name", value: "UserName_user" }, - }, { kind: "FragmentSpread", name: { kind: "Name", value: "UserEmailList_user" }, @@ -3516,30 +3435,6 @@ export const UserProfileQueryDocument = { ], }, }, - { - kind: "FragmentDefinition", - name: { kind: "Name", value: "UserName_user" }, - typeCondition: { - kind: "NamedType", - name: { kind: "Name", value: "User" }, - }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "id" } }, - { - kind: "Field", - name: { kind: "Name", value: "matrix" }, - selectionSet: { - kind: "SelectionSet", - selections: [ - { kind: "Field", name: { kind: "Name", value: "displayName" } }, - ], - }, - }, - ], - }, - }, { kind: "FragmentDefinition", name: { kind: "Name", value: "UserEmailList_user" }, @@ -4426,14 +4321,50 @@ export const CurrentUserGreetingDocument = { selections: [ { kind: "Field", - name: { kind: "Name", value: "viewer" }, + name: { kind: "Name", value: "viewerSession" }, selectionSet: { kind: "SelectionSet", selections: [ { kind: "Field", name: { kind: "Name", value: "__typename" } }, { - kind: "FragmentSpread", - name: { kind: "Name", value: "UserGreeting_user" }, + kind: "InlineFragment", + typeCondition: { + kind: "NamedType", + name: { kind: "Name", value: "BrowserSession" }, + }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { kind: "Field", name: { kind: "Name", value: "id" } }, + { + kind: "Field", + name: { kind: "Name", value: "user" }, + selectionSet: { + kind: "SelectionSet", + selections: [ + { + kind: "Field", + name: { kind: "Name", value: "id" }, + }, + { + kind: "FragmentSpread", + name: { + kind: "Name", + value: "UnverifiedEmailAlert_user", + }, + }, + { + kind: "FragmentSpread", + name: { + kind: "Name", + value: "UserGreeting_user", + }, + }, + ], + }, + }, + ], + }, }, ], }, @@ -4443,7 +4374,7 @@ export const CurrentUserGreetingDocument = { }, { kind: "FragmentDefinition", - name: { kind: "Name", value: "UnverifiedEmailAlert" }, + name: { kind: "Name", value: "UnverifiedEmailAlert_user" }, typeCondition: { kind: "NamedType", name: { kind: "Name", value: "User" }, @@ -4489,7 +4420,6 @@ export const CurrentUserGreetingDocument = { kind: "SelectionSet", selections: [ { kind: "Field", name: { kind: "Name", value: "id" } }, - { kind: "Field", name: { kind: "Name", value: "username" } }, { kind: "Field", name: { kind: "Name", value: "matrix" }, @@ -4501,10 +4431,6 @@ export const CurrentUserGreetingDocument = { ], }, }, - { - kind: "FragmentSpread", - name: { kind: "Name", value: "UnverifiedEmailAlert" }, - }, ], }, }, diff --git a/frontend/src/routes/_account.index.tsx b/frontend/src/routes/_account.index.tsx index f22db457b..ed1c6ade4 100644 --- a/frontend/src/routes/_account.index.tsx +++ b/frontend/src/routes/_account.index.tsx @@ -14,7 +14,7 @@ import { createFileRoute, notFound } from "@tanstack/react-router"; import IconKey from "@vector-im/compound-design-tokens/icons/key.svg?react"; -import { H3, Separator } from "@vector-im/compound-web"; +import { Separator } from "@vector-im/compound-web"; import { Suspense } from "react"; import { useTranslation } from "react-i18next"; import { useQuery } from "urql"; @@ -23,7 +23,6 @@ import BlockList from "../components/BlockList/BlockList"; import { ButtonLink } from "../components/ButtonLink"; import LoadingSpinner from "../components/LoadingSpinner"; import UserEmailList from "../components/UserProfile/UserEmailList"; -import UserName from "../components/UserProfile/UserName"; import { graphql } from "../gql"; const QUERY = graphql(/* GraphQL */ ` @@ -32,7 +31,6 @@ const QUERY = graphql(/* GraphQL */ ` __typename ... on User { id - ...UserName_user ...UserEmailList_user } } @@ -62,14 +60,12 @@ function Index(): React.ReactElement { return ( <> - - - -

{t("frontend.user_email_list.heading")}

+ {/* This wrapper is only needed for the anchor link */} +
}> - +
diff --git a/frontend/src/routes/_account.tsx b/frontend/src/routes/_account.tsx index 27fea9ad9..0807fc141 100644 --- a/frontend/src/routes/_account.tsx +++ b/frontend/src/routes/_account.tsx @@ -13,20 +13,33 @@ // limitations under the License. import { Outlet, createFileRoute, notFound } from "@tanstack/react-router"; +import { Heading } from "@vector-im/compound-web"; import { useTranslation } from "react-i18next"; import { useQuery } from "urql"; +import { useEndBrowserSession } from "../components/BrowserSession"; import Layout from "../components/Layout"; import NavBar from "../components/NavBar"; import NavItem from "../components/NavItem"; +import EndSessionButton from "../components/Session/EndSessionButton"; +import UnverifiedEmailAlert from "../components/UnverifiedEmailAlert"; import UserGreeting from "../components/UserGreeting"; import { graphql } from "../gql"; const QUERY = graphql(/* GraphQL */ ` query CurrentUserGreeting { - viewer { + viewerSession { __typename - ...UserGreeting_user + + ... on BrowserSession { + id + + user { + id + ...UnverifiedEmailAlert_user + ...UserGreeting_user + } + } } } `); @@ -39,7 +52,8 @@ export const Route = createFileRoute("/_account")({ { fetchOptions: { signal } }, ); if (result.error) throw result.error; - if (result.data?.viewer.__typename !== "User") throw notFound(); + if (result.data?.viewerSession.__typename !== "BrowserSession") + throw notFound(); }, component: Account, }); @@ -50,21 +64,34 @@ function Account(): React.ReactElement { query: QUERY, }); if (result.error) throw result.error; - const user = result.data?.viewer; - if (user?.__typename !== "User") throw notFound(); + const session = result.data?.viewerSession; + if (session?.__typename !== "BrowserSession") throw notFound(); + const onSessionEnd = useEndBrowserSession(session.id, true); return ( - +
+
+ + {t("frontend.account.title")} + + + +
+ + + + - - - {t("frontend.nav.profile")} - - - {t("frontend.nav.sessions")} - - + + + {t("frontend.nav.settings")} + + + {t("frontend.nav.devices")} + + +