Remove usage of tiley (#4406)

* First pass

* Mooarrr

* lint

* snapshots
This commit is contained in:
Tom Moor
2022-11-08 17:12:22 -08:00
committed by GitHub
parent 920b58c006
commit 587f062677
38 changed files with 169 additions and 177 deletions

View File

@@ -1,7 +1,9 @@
import { PlusIcon } from "outline-icons"; import { PlusIcon } from "outline-icons";
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { stringToColor } from "@shared/utils/color";
import TeamNew from "~/scenes/TeamNew"; import TeamNew from "~/scenes/TeamNew";
import TeamLogo from "~/components/TeamLogo";
import { createAction } from "~/actions"; import { createAction } from "~/actions";
import { loadSessionsFromCookie } from "~/hooks/useSessions"; import { loadSessionsFromCookie } from "~/hooks/useSessions";
import { TeamSection } from "../sections"; import { TeamSection } from "../sections";
@@ -11,7 +13,18 @@ export const switchTeamList = getSessions().map((session) => {
name: session.name, name: session.name,
section: TeamSection, section: TeamSection,
keywords: "change switch workspace organization team", keywords: "change switch workspace organization team",
icon: () => <Logo alt={session.name} src={session.logoUrl} />, icon: () => (
<StyledTeamLogo
alt={session.name}
model={{
initial: session.name[0],
avatarUrl: session.logoUrl,
id: session.teamId,
color: stringToColor(session.teamId),
}}
size={24}
/>
),
visible: ({ currentTeamId }) => currentTeamId !== session.teamId, visible: ({ currentTeamId }) => currentTeamId !== session.teamId,
perform: () => (window.location.href = session.url), perform: () => (window.location.href = session.url),
}); });
@@ -55,10 +68,9 @@ function getSessions(params?: { exclude?: string }) {
return otherSessions; return otherSessions;
} }
const Logo = styled("img")` const StyledTeamLogo = styled(TeamLogo)`
border-radius: 2px; border-radius: 2px;
width: 24px; border: 0;
height: 24px;
`; `;
export const rootTeamActions = [switchTeam, createTeam]; export const rootTeamActions = [switchTeam, createTeam];

View File

@@ -1,14 +1,21 @@
import * as React from "react"; import * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import User from "~/models/User";
import useBoolean from "~/hooks/useBoolean"; import useBoolean from "~/hooks/useBoolean";
import Initials from "./Initials";
import placeholder from "./placeholder.png"; import placeholder from "./placeholder.png";
export interface IAvatar {
avatarUrl: string | null;
color: string;
initial: string;
id: string;
}
type Props = { type Props = {
src: string;
size: number; size: number;
src?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
user?: User; model?: IAvatar;
alt?: string; alt?: string;
showBorder?: boolean; showBorder?: boolean;
onClick?: React.MouseEventHandler<HTMLImageElement>; onClick?: React.MouseEventHandler<HTMLImageElement>;
@@ -16,20 +23,28 @@ type Props = {
}; };
function Avatar(props: Props) { function Avatar(props: Props) {
const { src, icon, showBorder, ...rest } = props; const { icon, showBorder, model, ...rest } = props;
const src = props.src || model?.avatarUrl;
const [error, handleError] = useBoolean(false); const [error, handleError] = useBoolean(false);
return ( return (
<AvatarWrapper> <Relative>
<CircleImg {src ? (
onError={handleError} <CircleImg
src={error ? placeholder : src} onError={handleError}
$showBorder={showBorder} src={error ? placeholder : src}
{...rest} $showBorder={showBorder}
/> {...rest}
/>
) : model ? (
<Initials color={model.color} $showBorder={showBorder} {...rest}>
{model.initial}
</Initials>
) : (
<Initials $showBorder={showBorder} {...rest} />
)}
{icon && <IconWrapper>{icon}</IconWrapper>} {icon && <IconWrapper>{icon}</IconWrapper>}
</AvatarWrapper> </Relative>
); );
} }
@@ -37,7 +52,7 @@ Avatar.defaultProps = {
size: 24, size: 24,
}; };
const AvatarWrapper = styled.div` const Relative = styled.div`
position: relative; position: relative;
`; `;

View File

@@ -51,7 +51,7 @@ function AvatarWithPresence({
$isObserving={isObserving} $isObserving={isObserving}
$color={user.color} $color={user.color}
> >
<Avatar src={user.avatarUrl} onClick={onClick} size={32} /> <Avatar model={user} onClick={onClick} size={32} />
</AvatarWrapper> </AvatarWrapper>
</Tooltip> </Tooltip>
</> </>

View File

@@ -0,0 +1,27 @@
import styled from "styled-components";
import Flex from "~/components/Flex";
const Initials = styled(Flex)<{
color?: string;
size: number;
$showBorder?: boolean;
}>`
align-items: center;
justify-content: center;
border-radius: 50%;
width: 100%;
height: 100%;
color: #fff;
background-color: ${(props) => props.color};
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
border: 2px solid
${(props) =>
props.$showBorder === false ? "transparent" : props.theme.background};
flex-shrink: 0;
font-size: ${(props) => props.size / 2}px;
font-weight: 500;
`;
export default Initials;

View File

@@ -43,10 +43,10 @@ function DocumentViews({ document, isOpen }: Props) {
<PaginatedList <PaginatedList
aria-label={t("Viewers")} aria-label={t("Viewers")}
items={users} items={users}
renderItem={(item: User) => { renderItem={(model: User) => {
const view = documentViews.find((v) => v.user.id === item.id); const view = documentViews.find((v) => v.user.id === model.id);
const isPresent = presentIds.includes(item.id); const isPresent = presentIds.includes(model.id);
const isEditing = editingIds.includes(item.id); const isEditing = editingIds.includes(model.id);
const subtitle = isPresent const subtitle = isPresent
? isEditing ? isEditing
? t("Currently editing") ? t("Currently editing")
@@ -58,10 +58,10 @@ function DocumentViews({ document, isOpen }: Props) {
}); });
return ( return (
<ListItem <ListItem
key={item.id} key={model.id}
title={item.name} title={model.name}
subtitle={subtitle} subtitle={subtitle}
image={<Avatar key={item.id} src={item.avatarUrl} size={32} />} image={<Avatar key={model.id} model={model} size={32} />}
border={false} border={false}
small small
/> />

View File

@@ -142,7 +142,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
onClick={handleTimeClick} onClick={handleTimeClick}
/> />
} }
image={<Avatar src={event.actor?.avatarUrl} size={32} />} image={<Avatar model={event.actor} size={32} />}
subtitle={ subtitle={
<Subtitle> <Subtitle>
{icon} {icon}

View File

@@ -39,7 +39,7 @@ function Facepile({
} }
function DefaultAvatar(user: User) { function DefaultAvatar(user: User) {
return <Avatar user={user} src={user.avatarUrl} size={32} />; return <Avatar model={user} size={32} />;
} }
const AvatarWrapper = styled.div` const AvatarWrapper = styled.div`

View File

@@ -63,14 +63,7 @@ function AppSidebar() {
<SidebarButton <SidebarButton
{...props} {...props}
title={team.name} title={team.name}
image={ image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
<StyledTeamLogo
src={team.avatarUrl}
width={32}
height={32}
alt={t("Logo")}
/>
}
showDisclosure showDisclosure
/> />
)} )}
@@ -139,11 +132,6 @@ function AppSidebar() {
); );
} }
const StyledTeamLogo = styled(TeamLogo)`
margin-right: 4px;
background: white;
`;
const Drafts = styled(Text)` const Drafts = styled(Text)`
margin: 0 4px; margin: 0 4px;
`; `;

View File

@@ -178,7 +178,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
image={ image={
<StyledAvatar <StyledAvatar
alt={user.name} alt={user.name}
src={user.avatarUrl} model={user}
size={24} size={24}
showBorder={false} showBorder={false}
/> />

View File

@@ -1,10 +1,7 @@
import styled from "styled-components"; import styled from "styled-components";
import Avatar from "./Avatar";
const TeamLogo = styled.img<{ width?: number; height?: number; size?: string }>` const TeamLogo = styled(Avatar)`
width: ${(props) =>
props.width ? `${props.width}px` : props.size || "auto"};
height: ${(props) =>
props.height ? `${props.height}px` : props.size || "38px"};
border-radius: 4px; border-radius: 4px;
border: 1px solid ${(props) => props.theme.divider}; border: 1px solid ${(props) => props.theme.divider};
overflow: hidden; overflow: hidden;

View File

@@ -1,5 +1,6 @@
import { computed, observable } from "mobx"; import { computed, observable } from "mobx";
import { TeamPreference, TeamPreferences } from "@shared/types"; import { TeamPreference, TeamPreferences } from "@shared/types";
import { stringToColor } from "@shared/utils/color";
import BaseModel from "./BaseModel"; import BaseModel from "./BaseModel";
import Field from "./decorators/Field"; import Field from "./decorators/Field";
@@ -69,6 +70,16 @@ class Team extends BaseModel {
return "SSO"; return "SSO";
} }
@computed
get color(): string {
return stringToColor(this.id);
}
@computed
get initial(): string {
return this.name ? this.name[0] : "?";
}
/** /**
* Returns whether this team is using a separate editing mode behind an "Edit" * Returns whether this team is using a separate editing mode behind an "Edit"
* button rather than seamless always-editing. * button rather than seamless always-editing.

View File

@@ -40,6 +40,11 @@ class User extends ParanoidModel {
isSuspended: boolean; isSuspended: boolean;
@computed
get initial(): string {
return this.name ? this.name[0] : "?";
}
@computed @computed
get isInvited(): boolean { get isInvited(): boolean {
return !this.lastActiveAt; return !this.lastActiveAt;

View File

@@ -104,18 +104,16 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
users={sortBy(collectionUsers, "lastActiveAt")} users={sortBy(collectionUsers, "lastActiveAt")}
overflow={overflow} overflow={overflow}
limit={limit} limit={limit}
renderAvatar={(user) => ( renderAvatar={(user) => <StyledAvatar model={user} size={32} />}
<StyledAvatar user={user} src={user.avatarUrl} size={32} />
)}
/> />
</Fade> </Fade>
</NudeButton> </NudeButton>
); );
}; };
const StyledAvatar = styled(Avatar)<{ user: User }>` const StyledAvatar = styled(Avatar)<{ model: User }>`
transition: opacity 250ms ease-in-out; transition: opacity 250ms ease-in-out;
opacity: ${(props) => (props.user.isRecentlyActive ? 1 : 0.5)}; opacity: ${(props) => (props.model.isRecentlyActive ? 1 : 0.5)};
`; `;
export default observer(MembershipPreview); export default observer(MembershipPreview);

View File

@@ -49,7 +49,7 @@ const MemberListItem = ({
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>} {user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</> </>
} }
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar model={user} size={32} />}
actions={ actions={
<Flex align="center" gap={8}> <Flex align="center" gap={8}>
{onUpdate && ( {onUpdate && (

View File

@@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
return ( return (
<ListItem <ListItem
title={user.name} title={user.name}
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar model={user} size={32} />}
subtitle={ subtitle={
<> <>
{user.lastActiveAt ? ( {user.lastActiveAt ? (

View File

@@ -35,7 +35,7 @@ const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>} {user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
</> </>
} }
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar model={user} size={32} />}
actions={ actions={
<Flex align="center"> <Flex align="center">
{onRemove && <GroupMemberMenu onRemove={onRemove} />} {onRemove && <GroupMemberMenu onRemove={onRemove} />}

View File

@@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
return ( return (
<ListItem <ListItem
title={user.name} title={user.name}
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar model={user} size={32} />}
subtitle={ subtitle={
<> <>
{user.lastActiveAt ? ( {user.lastActiveAt ? (

View File

@@ -169,7 +169,7 @@ function Login({ children }: Props) {
/> />
<Logo> <Logo>
{config.logo ? ( {config.logo ? (
<TeamLogo width={48} height={48} src={config.logo} /> <TeamLogo size={48} src={config.logo} />
) : ( ) : (
<OutlineLogo size={42} fill="currentColor" /> <OutlineLogo size={42} fill="currentColor" />
)} )}

View File

@@ -26,7 +26,6 @@ function Details() {
const form = useRef<HTMLFormElement>(null); const form = useRef<HTMLFormElement>(null);
const [name, setName] = useState(team.name); const [name, setName] = useState(team.name);
const [subdomain, setSubdomain] = useState(team.subdomain); const [subdomain, setSubdomain] = useState(team.subdomain);
const [avatarUrl, setAvatarUrl] = useState<string>(team.avatarUrl);
const [defaultCollectionId, setDefaultCollectionId] = useState<string | null>( const [defaultCollectionId, setDefaultCollectionId] = useState<string | null>(
team.defaultCollectionId team.defaultCollectionId
); );
@@ -40,7 +39,6 @@ function Details() {
try { try {
await auth.updateTeam({ await auth.updateTeam({
name, name,
avatarUrl,
subdomain, subdomain,
defaultCollectionId, defaultCollectionId,
}); });
@@ -53,7 +51,7 @@ function Details() {
}); });
} }
}, },
[auth, name, avatarUrl, subdomain, defaultCollectionId, showToast, t] [auth, name, subdomain, defaultCollectionId, showToast, t]
); );
const handleNameChange = React.useCallback( const handleNameChange = React.useCallback(
@@ -71,7 +69,6 @@ function Details() {
); );
const handleAvatarUpload = async (avatarUrl: string) => { const handleAvatarUpload = async (avatarUrl: string) => {
setAvatarUrl(avatarUrl);
await auth.updateTeam({ await auth.updateTeam({
avatarUrl, avatarUrl,
}); });
@@ -115,7 +112,7 @@ function Details() {
<ImageInput <ImageInput
onSuccess={handleAvatarUpload} onSuccess={handleAvatarUpload}
onError={handleAvatarError} onError={handleAvatarError}
src={avatarUrl} model={team}
borderRadius={0} borderRadius={0}
/> />
</SettingRow> </SettingRow>

View File

@@ -18,7 +18,6 @@ const Profile = () => {
const user = useCurrentUser(); const user = useCurrentUser();
const form = React.useRef<HTMLFormElement>(null); const form = React.useRef<HTMLFormElement>(null);
const [name, setName] = React.useState<string>(user.name || ""); const [name, setName] = React.useState<string>(user.name || "");
const [avatarUrl, setAvatarUrl] = React.useState<string>(user.avatarUrl);
const { showToast } = useToasts(); const { showToast } = useToasts();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -28,7 +27,6 @@ const Profile = () => {
try { try {
await auth.updateUser({ await auth.updateUser({
name, name,
avatarUrl,
}); });
showToast(t("Profile saved"), { showToast(t("Profile saved"), {
type: "success", type: "success",
@@ -45,7 +43,6 @@ const Profile = () => {
}; };
const handleAvatarUpload = async (avatarUrl: string) => { const handleAvatarUpload = async (avatarUrl: string) => {
setAvatarUrl(avatarUrl);
await auth.updateUser({ await auth.updateUser({
avatarUrl, avatarUrl,
}); });
@@ -79,7 +76,7 @@ const Profile = () => {
<ImageInput <ImageInput
onSuccess={handleAvatarUpload} onSuccess={handleAvatarUpload}
onError={handleAvatarError} onError={handleAvatarError}
src={avatarUrl} model={user}
/> />
</SettingRow> </SettingRow>
<SettingRow <SettingRow

View File

@@ -1,21 +1,22 @@
import * as React from "react"; import * as React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styled from "styled-components"; import styled from "styled-components";
import Avatar, { IAvatar } from "~/components/Avatar/Avatar";
import Flex from "~/components/Flex"; import Flex from "~/components/Flex";
import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload"; import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload";
type Props = ImageUploadProps & { type Props = ImageUploadProps & {
src?: string; model: IAvatar;
}; };
export default function ImageInput({ src, ...rest }: Props) { export default function ImageInput({ model, ...rest }: Props) {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<ImageBox> <ImageBox>
<ImageUpload {...rest}> <ImageUpload {...rest}>
<Avatar src={src} /> <StyledAvatar model={model} size={64} />
<Flex auto align="center" justify="center"> <Flex auto align="center" justify="center" className="upload">
{t("Upload")} {t("Upload")}
</Flex> </Flex>
</ImageUpload> </ImageUpload>
@@ -28,8 +29,8 @@ const avatarStyles = `
height: 64px; height: 64px;
`; `;
const Avatar = styled.img` const StyledAvatar = styled(Avatar)`
${avatarStyles}; border-radius: 8px;
`; `;
const ImageBox = styled(Flex)` const ImageBox = styled(Flex)`
@@ -41,7 +42,7 @@ const ImageBox = styled(Flex)`
background: ${(props) => props.theme.background}; background: ${(props) => props.theme.background};
overflow: hidden; overflow: hidden;
div div { .upload {
${avatarStyles}; ${avatarStyles};
position: absolute; position: absolute;
top: 0; top: 0;
@@ -53,7 +54,7 @@ const ImageBox = styled(Flex)`
transition: all 250ms; transition: all 250ms;
} }
&:hover div { &:hover .upload {
opacity: 1; opacity: 1;
background: rgba(0, 0, 0, 0.75); background: rgba(0, 0, 0, 0.75);
color: ${(props) => props.theme.white}; color: ${(props) => props.theme.white};

View File

@@ -29,7 +29,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
Cell: observer( Cell: observer(
({ value, row }: { value: string; row: { original: User } }) => ( ({ value, row }: { value: string; row: { original: User } }) => (
<Flex align="center" gap={8}> <Flex align="center" gap={8}>
<Avatar src={row.original.avatarUrl} size={32} /> {value}{" "} <Avatar model={row.original} size={32} /> {value}{" "}
{currentUser.id === row.original.id && `(${t("You")})`} {currentUser.id === row.original.id && `(${t("You")})`}
</Flex> </Flex>
) )

View File

@@ -39,7 +39,7 @@ function SharesTable({ canManage, ...rest }: Props) {
<Flex align="center" gap={4}> <Flex align="center" gap={4}>
{row.original.createdBy && ( {row.original.createdBy && (
<Avatar <Avatar
src={row.original.createdBy.avatarUrl} model={row.original.createdBy}
alt={row.original.createdBy.name} alt={row.original.createdBy.name}
/> />
)} )}

View File

@@ -20,7 +20,7 @@ const UserListItem = ({ user, showMenu }: Props) => {
return ( return (
<ListItem <ListItem
title={<Title>{user.name}</Title>} title={<Title>{user.name}</Title>}
image={<Avatar src={user.avatarUrl} size={32} />} image={<Avatar model={user} size={32} />}
subtitle={ subtitle={
<> <>
{user.email ? `${user.email} · ` : undefined} {user.email ? `${user.email} · ` : undefined}

View File

@@ -39,7 +39,7 @@ function UserProfile(props: Props) {
<Modal <Modal
title={ title={
<Flex align="center"> <Flex align="center">
<Avatar src={user.avatarUrl} size={38} alt={t("Profile picture")} /> <Avatar model={user} size={38} alt={t("Profile picture")} />
<span>&nbsp;{user.name}</span> <span>&nbsp;{user.name}</span>
</Flex> </Flex>
} }

View File

@@ -226,14 +226,17 @@ export default class AuthStore {
preferences?: UserPreferences; preferences?: UserPreferences;
}) => { }) => {
this.isSaving = true; this.isSaving = true;
const previousData = this.user?.toAPI();
try { try {
this.user?.updateFromJson(params);
const res = await client.post(`/users.update`, params); const res = await client.post(`/users.update`, params);
invariant(res?.data, "User response not available"); invariant(res?.data, "User response not available");
runInAction("AuthStore#updateUser", () => { this.user?.updateFromJson(res.data);
this.addPolicies(res.policies); this.addPolicies(res.policies);
this.user = new User(res.data, this); } catch (err) {
}); this.user?.updateFromJson(previousData);
throw err;
} finally { } finally {
this.isSaving = false; this.isSaving = false;
} }
@@ -251,14 +254,17 @@ export default class AuthStore {
preferences?: TeamPreferences; preferences?: TeamPreferences;
}) => { }) => {
this.isSaving = true; this.isSaving = true;
const previousData = this.team?.toAPI();
try { try {
this.team?.updateFromJson(params);
const res = await client.post(`/team.update`, params); const res = await client.post(`/team.update`, params);
invariant(res?.data, "Team response not available"); invariant(res?.data, "Team response not available");
runInAction("AuthStore#updateTeam", () => { this.team?.updateFromJson(res.data);
this.addPolicies(res.policies); this.addPolicies(res.policies);
this.team = new Team(res.data, this); } catch (err) {
}); this.team?.updateFromJson(previousData);
throw err;
} finally { } finally {
this.isSaving = false; this.isSaving = false;
} }

View File

@@ -40,7 +40,6 @@ async function teamCreator({
// one via ClearBit, or fallback to colored initials in worst case scenario // one via ClearBit, or fallback to colored initials in worst case scenario
if (!avatarUrl || !avatarUrl.startsWith("http")) { if (!avatarUrl || !avatarUrl.startsWith("http")) {
avatarUrl = await generateAvatarUrl({ avatarUrl = await generateAvatarUrl({
name,
domain, domain,
id: subdomain, id: subdomain,
}); });

View File

@@ -329,13 +329,6 @@ export class Environment {
*/ */
public RELEASE = this.toOptionalString(process.env.RELEASE); public RELEASE = this.toOptionalString(process.env.RELEASE);
/**
* An optional host from which to load default avatars.
*/
@IsUrl()
public DEFAULT_AVATAR_HOST =
process.env.DEFAULT_AVATAR_HOST ?? "https://tiley.herokuapp.com";
/** /**
* A Google Analytics tracking ID, only v3 supported at this time. * A Google Analytics tracking ID, only v3 supported at this time.
*/ */

View File

@@ -24,7 +24,6 @@ import { CollectionPermission, TeamPreference } from "@shared/types";
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains"; import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
import env from "@server/env"; import env from "@server/env";
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask"; import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
import { generateAvatarUrl } from "@server/utils/avatars";
import parseAttachmentIds from "@server/utils/parseAttachmentIds"; import parseAttachmentIds from "@server/utils/parseAttachmentIds";
import Attachment from "./Attachment"; import Attachment from "./Attachment";
import AuthenticationProvider from "./AuthenticationProvider"; import AuthenticationProvider from "./AuthenticationProvider";
@@ -94,8 +93,20 @@ class Team extends ParanoidModel {
@AllowNull @AllowNull
@IsUrl @IsUrl
@Length({ max: 4096, msg: "avatarUrl must be 4096 characters or less" }) @Length({ max: 4096, msg: "avatarUrl must be 4096 characters or less" })
@Column @Column(DataType.STRING)
avatarUrl: string | null; get avatarUrl() {
const original = this.getDataValue("avatarUrl");
if (original && !original.startsWith("https://tiley.herokuapp.com")) {
return original;
}
return null;
}
set avatarUrl(value: string | null) {
this.setDataValue("avatarUrl", value);
}
@Default(true) @Default(true)
@Column @Column
@@ -163,16 +174,6 @@ class Team extends ParanoidModel {
return url.href.replace(/\/$/, ""); return url.href.replace(/\/$/, "");
} }
get logoUrl() {
return (
this.avatarUrl ||
generateAvatarUrl({
id: this.id,
name: this.name,
})
);
}
/** /**
* Preferences that decide behavior for the team. * Preferences that decide behavior for the team.
* *

View File

@@ -180,17 +180,11 @@ class User extends ParanoidModel {
get avatarUrl() { get avatarUrl() {
const original = this.getDataValue("avatarUrl"); const original = this.getDataValue("avatarUrl");
if (original) { if (original && !original.startsWith("https://tiley.herokuapp.com")) {
return original; return original;
} }
const color = this.color.replace(/^#/, ""); return null;
const initial = this.name ? this.name[0] : "?";
const hash = crypto
.createHash("md5")
.update(this.email || "")
.digest("hex");
return `${env.DEFAULT_AVATAR_HOST}/avatar/${hash}/${initial}.png?c=${color}`;
} }
set avatarUrl(value: string | null) { set avatarUrl(value: string | null) {

View File

@@ -2,7 +2,7 @@
exports[`presents a user 1`] = ` exports[`presents a user 1`] = `
Object { Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80", "avatarUrl": null,
"color": "#FF5C80", "color": "#FF5C80",
"createdAt": undefined, "createdAt": undefined,
"id": "123", "id": "123",
@@ -17,7 +17,7 @@ Object {
exports[`presents a user without slack data 1`] = ` exports[`presents a user without slack data 1`] = `
Object { Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80", "avatarUrl": null,
"color": "#FF5C80", "color": "#FF5C80",
"createdAt": undefined, "createdAt": undefined,
"id": "123", "id": "123",

View File

@@ -4,7 +4,7 @@ export default function present(team: Team, isSignedIn = false) {
return { return {
id: team.id, id: team.id,
name: team.name, name: team.name,
avatarUrl: team.logoUrl, avatarUrl: team.avatarUrl,
url: team.url, url: team.url,
isSignedIn, isSignedIn,
}; };

View File

@@ -4,7 +4,7 @@ export default function present(team: Team) {
return { return {
id: team.id, id: team.id,
name: team.name, name: team.name,
avatarUrl: team.logoUrl, avatarUrl: team.avatarUrl,
sharing: team.sharing, sharing: team.sharing,
memberCollectionCreate: team.memberCollectionCreate, memberCollectionCreate: team.memberCollectionCreate,
collaborativeEditing: team.collaborativeEditing, collaborativeEditing: team.collaborativeEditing,

View File

@@ -3,7 +3,7 @@
exports[`#users.activate should activate a suspended user 1`] = ` exports[`#users.activate should activate a suspended user 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",
@@ -59,7 +59,7 @@ Object {
exports[`#users.demote should demote an admin 1`] = ` exports[`#users.demote should demote an admin 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",
@@ -97,7 +97,7 @@ Object {
exports[`#users.demote should demote an admin to member 1`] = ` exports[`#users.demote should demote an admin to member 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",
@@ -135,7 +135,7 @@ Object {
exports[`#users.demote should demote an admin to viewer 1`] = ` exports[`#users.demote should demote an admin to viewer 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",
@@ -191,7 +191,7 @@ Object {
exports[`#users.promote should promote a new admin 1`] = ` exports[`#users.promote should promote a new admin 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",
@@ -256,7 +256,7 @@ Object {
exports[`#users.suspend should suspend an user 1`] = ` exports[`#users.suspend should suspend an user 1`] = `
Object { Object {
"data": Object { "data": Object {
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0", "avatarUrl": null,
"color": "#e600e0", "color": "#e600e0",
"createdAt": "2018-01-02T00:00:00.000Z", "createdAt": "2018-01-02T00:00:00.000Z",
"email": "user1@example.com", "email": "user1@example.com",

View File

@@ -93,7 +93,7 @@ export async function signIn(
...existing, ...existing,
[team.id]: { [team.id]: {
name: team.name, name: team.name,
logoUrl: team.logoUrl, logoUrl: team.avatarUrl,
url: team.url, url: team.url,
}, },
}) })

View File

@@ -4,43 +4,6 @@ it("should return clearbit url if available", async () => {
const url = await generateAvatarUrl({ const url = await generateAvatarUrl({
id: "google", id: "google",
domain: "google.com", domain: "google.com",
name: "Google",
}); });
expect(url).toBe("https://logo.clearbit.com/google.com"); expect(url).toBe("https://logo.clearbit.com/google.com");
}); });
it("should return tiley url if clearbit unavailable", async () => {
const url = await generateAvatarUrl({
id: "invalid",
domain: "example.invalid",
name: "Invalid",
});
expect(url).toBe(
"https://tiley.herokuapp.com/avatar/f1234d75178d892a133a410355a5a990cf75d2f33eba25d575943d4df632f3a4/I.png"
);
});
it("should return tiley url if domain not provided", async () => {
const url = await generateAvatarUrl({
id: "google",
name: "Google",
});
expect(url).toBe(
"https://tiley.herokuapp.com/avatar/bbdefa2950f49882f295b1285d4fa9dec45fc4144bfb07ee6acc68762d12c2e3/G.png"
);
});
it("should return tiley url if name not provided", async () => {
const url = await generateAvatarUrl({
id: "google",
});
expect(url).toBe(
"https://tiley.herokuapp.com/avatar/bbdefa2950f49882f295b1285d4fa9dec45fc4144bfb07ee6acc68762d12c2e3/U.png"
);
});
it("should return tiley url with encoded name", async () => {
const url = await generateAvatarUrl({
id: "google",
name: "株",
});
expect(url).toBe(
"https://tiley.herokuapp.com/avatar/bbdefa2950f49882f295b1285d4fa9dec45fc4144bfb07ee6acc68762d12c2e3/%E6%A0%AA.png"
);
});

View File

@@ -1,21 +1,17 @@
import crypto from "crypto"; import crypto from "crypto";
import fetch from "fetch-with-proxy"; import fetch from "fetch-with-proxy";
import env from "@server/env";
export async function generateAvatarUrl({ export async function generateAvatarUrl({
id, id,
domain, domain,
name = "Unknown",
}: { }: {
id: string; id: string;
domain?: string; domain?: string;
name?: string;
}) { }) {
// attempt to get logo from Clearbit API. If one doesn't exist then // attempt to get logo from Clearbit API. If one doesn't exist then
// fall back to using tiley to generate a placeholder logo // fall back to using tiley to generate a placeholder logo
const hash = crypto.createHash("sha256"); const hash = crypto.createHash("sha256");
hash.update(id); hash.update(id);
const hashedId = hash.digest("hex");
let cbResponse, cbUrl; let cbResponse, cbUrl;
if (domain) { if (domain) {
@@ -28,8 +24,5 @@ export async function generateAvatarUrl({
} }
} }
const tileyUrl = `${ return cbUrl && cbResponse && cbResponse.status === 200 ? cbUrl : null;
env.DEFAULT_AVATAR_HOST
}/avatar/${hashedId}/${encodeURIComponent(name[0])}.png`;
return cbUrl && cbResponse && cbResponse.status === 200 ? cbUrl : tileyUrl;
} }

View File

@@ -6,7 +6,6 @@ import fetch from "fetch-with-proxy";
import { compact } from "lodash"; import { compact } from "lodash";
import { useAgent } from "request-filtering-agent"; import { useAgent } from "request-filtering-agent";
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import env from "@server/env";
import Logger from "@server/logging/Logger"; import Logger from "@server/logging/Logger";
const AWS_S3_ACCELERATE_URL = process.env.AWS_S3_ACCELERATE_URL; const AWS_S3_ACCELERATE_URL = process.env.AWS_S3_ACCELERATE_URL;
@@ -184,11 +183,7 @@ export const uploadToS3FromUrl = async (
acl: string acl: string
) => { ) => {
const endpoint = publicS3Endpoint(true); const endpoint = publicS3Endpoint(true);
if ( if (url.startsWith("/api") || url.startsWith(endpoint)) {
url.startsWith("/api") ||
url.startsWith(endpoint) ||
url.startsWith(env.DEFAULT_AVATAR_HOST)
) {
return; return;
} }