From 07ae67924fac2d79c6a6bd3e71d20dc78275a885 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Mon, 8 May 2023 14:46:25 -0400 Subject: [PATCH] Use team name and favicon (when public branding enabled) on shared links --- app/components/PageTitle.tsx | 25 ++++++++-------- app/components/Sidebar/Shared.tsx | 6 ++-- app/components/TeamContext.ts | 11 +++++++ app/scenes/Document/Shared.tsx | 48 +++++++++++++++---------------- app/stores/DocumentsStore.ts | 7 ++--- server/routes/app.ts | 13 +++++++-- server/static/index.html | 2 +- shared/types.ts | 6 ++++ 8 files changed, 69 insertions(+), 49 deletions(-) create mode 100644 app/components/TeamContext.ts diff --git a/app/components/PageTitle.tsx b/app/components/PageTitle.tsx index 47e94ec42..24fbd2901 100644 --- a/app/components/PageTitle.tsx +++ b/app/components/PageTitle.tsx @@ -1,35 +1,34 @@ import { observer } from "mobx-react"; import * as React from "react"; import { Helmet } from "react-helmet-async"; -import { cdnPath } from "@shared/utils/urls"; import env from "~/env"; import useStores from "~/hooks/useStores"; +import { useTeamContext } from "./TeamContext"; type Props = { title: React.ReactNode; favicon?: string; }; +const originalShortcutHref = document + .querySelector('link[rel="shortcut icon"]') + ?.getAttribute("href") as string; + const PageTitle = ({ title, favicon }: Props) => { const { auth } = useStores(); - const { team } = auth; + const team = useTeamContext() ?? auth.team; return ( {team?.name ? `${title} - ${team.name}` : `${title} - ${env.APP_NAME}`} - {favicon ? ( - - ) : ( - - )} + ); diff --git a/app/components/Sidebar/Shared.tsx b/app/components/Sidebar/Shared.tsx index 314e88b46..0618beb9a 100644 --- a/app/components/Sidebar/Shared.tsx +++ b/app/components/Sidebar/Shared.tsx @@ -8,7 +8,7 @@ import SearchPopover from "~/components/SearchPopover"; import useStores from "~/hooks/useStores"; import history from "~/utils/history"; import { homePath, sharedDocumentPath } from "~/utils/routeHelpers"; -import { IAvatar } from "../Avatar/Avatar"; +import { useTeamContext } from "../TeamContext"; import TeamLogo from "../TeamLogo"; import Sidebar from "./Sidebar"; import HeaderButton from "./components/HeaderButton"; @@ -16,12 +16,12 @@ import Section from "./components/Section"; import DocumentLink from "./components/SharedDocumentLink"; type Props = { - team?: IAvatar & { name: string }; rootNode: NavigationNode; shareId: string; }; -function SharedSidebar({ rootNode, team, shareId }: Props) { +function SharedSidebar({ rootNode, shareId }: Props) { + const team = useTeamContext(); const { ui, documents, auth } = useStores(); const { t } = useTranslation(); diff --git a/app/components/TeamContext.ts b/app/components/TeamContext.ts new file mode 100644 index 000000000..913350326 --- /dev/null +++ b/app/components/TeamContext.ts @@ -0,0 +1,11 @@ +import * as React from "react"; +import { PublicTeam } from "@shared/types"; +import Team from "~/models/Team"; + +export const TeamContext = React.createContext( + undefined +); + +export function useTeamContext() { + return React.useContext(TeamContext); +} diff --git a/app/scenes/Document/Shared.tsx b/app/scenes/Document/Shared.tsx index f607c4317..4ddddd7ac 100644 --- a/app/scenes/Document/Shared.tsx +++ b/app/scenes/Document/Shared.tsx @@ -7,12 +7,13 @@ import { RouteComponentProps, useLocation, Redirect } from "react-router-dom"; import styled, { ThemeProvider } from "styled-components"; import { setCookie } from "tiny-cookie"; import { s } from "@shared/styles"; -import { CustomTheme, NavigationNode } from "@shared/types"; +import { NavigationNode, PublicTeam } from "@shared/types"; import DocumentModel from "~/models/Document"; import Error404 from "~/scenes/Error404"; import ErrorOffline from "~/scenes/ErrorOffline"; import Layout from "~/components/Layout"; import Sidebar from "~/components/Sidebar/Shared"; +import { TeamContext } from "~/components/TeamContext"; import Text from "~/components/Text"; import env from "~/env"; import useBuildTheme from "~/hooks/useBuildTheme"; @@ -29,11 +30,7 @@ const EMPTY_OBJECT = {}; type Response = { document: DocumentModel; - team?: { - name: string; - avatarUrl: string; - customTheme?: Partial; - }; + team?: PublicTeam; sharedTree?: NavigationNode | undefined; }; @@ -166,14 +163,6 @@ function SharedDocumentScene(props: Props) { return ; } - const sidebar = response.sharedTree?.children.length ? ( - - ) : undefined; - return ( <> @@ -182,17 +171,26 @@ function SharedDocumentScene(props: Props) { href={canonicalOrigin + location.pathname.replace(/\/$/, "")} /> - - - - - + + + + ) : undefined + } + > + + + + ); } diff --git a/app/stores/DocumentsStore.ts b/app/stores/DocumentsStore.ts index 1cda37d44..e630bbf39 100644 --- a/app/stores/DocumentsStore.ts +++ b/app/stores/DocumentsStore.ts @@ -1,7 +1,7 @@ import invariant from "invariant"; import { find, orderBy, filter, compact, omitBy } from "lodash"; import { observable, action, computed, runInAction } from "mobx"; -import { DateFilter, NavigationNode } from "@shared/types"; +import { DateFilter, NavigationNode, PublicTeam } from "@shared/types"; import { subtractDate } from "@shared/utils/date"; import { bytesToHumanReadable } from "@shared/utils/files"; import naturalSort from "@shared/utils/naturalSort"; @@ -9,7 +9,6 @@ import { DocumentValidation } from "@shared/validations"; import BaseStore from "~/stores/BaseStore"; import RootStore from "~/stores/RootStore"; import Document from "~/models/Document"; -import Team from "~/models/Team"; import env from "~/env"; import { FetchOptions, PaginationParams, SearchResult } from "~/types"; import { client } from "~/utils/ApiClient"; @@ -38,7 +37,7 @@ type ImportOptions = { export default class DocumentsStore extends BaseStore { sharedCache: Map< string, - { sharedTree: NavigationNode; team: Team } | undefined + { sharedTree: NavigationNode; team: PublicTeam } | undefined > = new Map(); @observable @@ -471,7 +470,7 @@ export default class DocumentsStore extends BaseStore { options: FetchOptions = {} ): Promise<{ document: Document; - team?: Team; + team?: PublicTeam; sharedTree?: NavigationNode; }> => { if (!options.prefetch) { diff --git a/server/routes/app.ts b/server/routes/app.ts index 532fc2309..9596766ee 100644 --- a/server/routes/app.ts +++ b/server/routes/app.ts @@ -5,7 +5,7 @@ import { Context, Next } from "koa"; import { escape } from "lodash"; import { Sequelize } from "sequelize"; import isUUID from "validator/lib/isUUID"; -import { IntegrationType } from "@shared/types"; +import { IntegrationType, TeamPreference } from "@shared/types"; import documentLoader from "@server/commands/documentLoader"; import env from "@server/env"; import { Integration } from "@server/models"; @@ -49,6 +49,7 @@ export const renderApp = async ( title?: string; description?: string; canonical?: string; + shortcutIcon?: string; analytics?: Integration | null; } = {} ) => { @@ -56,6 +57,7 @@ export const renderApp = async ( title = env.APP_NAME, description = "A modern team knowledge base for your internal documentation, product specs, support answers, meeting notes, onboarding, & more…", canonical = "", + shortcutIcon = `${env.CDN_URL || ""}/images/favicon-32.png`, } = options; if (ctx.request.path === "/realtime/") { @@ -91,6 +93,7 @@ export const renderApp = async ( .replace(/\{title\}/g, escape(title)) .replace(/\{description\}/g, escape(description)) .replace(/\{canonical-url\}/g, canonical) + .replace(/\{shortcut-icon\}/g, shortcutIcon) .replace(/\{prefetch\}/g, shareId ? "" : prefetchTags) .replace(/\{slack-app-id\}/g, env.SLACK_APP_ID || "") .replace(/\{cdn-url\}/g, env.CDN_URL || "") @@ -102,10 +105,10 @@ export const renderShare = async (ctx: Context, next: Next) => { // Find the share record if publicly published so that the document title // can be be returned in the server-rendered HTML. This allows it to appear in // unfurls with more reliablity - let share, document, analytics; + let share, document, team, analytics; try { - const team = await getTeamFromContext(ctx); + team = await getTeamFromContext(ctx); const result = await documentLoader({ id: documentSlug, shareId, @@ -146,6 +149,10 @@ export const renderShare = async (ctx: Context, next: Next) => { return renderApp(ctx, next, { title: document?.title, description: document?.getSummary(), + shortcutIcon: + team?.getPreference(TeamPreference.PublicBranding) && team.avatarUrl + ? team.avatarUrl + : undefined, analytics, canonical: share ? `${share.canonicalUrl}${documentSlug && document ? document.url : ""}` diff --git a/server/static/index.html b/server/static/index.html index 3c30ff353..b457231a4 100644 --- a/server/static/index.html +++ b/server/static/index.html @@ -14,7 +14,7 @@ ; +}; + export enum TeamPreference { /** Whether documents have a separate edit mode instead of seamless editing. */ SeamlessEdit = "seamlessEdit",