mirror of
https://github.com/outline/outline.git
synced 2025-03-28 14:34:35 +00:00
Remove usage of tiley (#4406)
* First pass * Mooarrr * lint * snapshots
This commit is contained in:
@ -1,7 +1,9 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import TeamNew from "~/scenes/TeamNew";
|
||||
import TeamLogo from "~/components/TeamLogo";
|
||||
import { createAction } from "~/actions";
|
||||
import { loadSessionsFromCookie } from "~/hooks/useSessions";
|
||||
import { TeamSection } from "../sections";
|
||||
@ -11,7 +13,18 @@ export const switchTeamList = getSessions().map((session) => {
|
||||
name: session.name,
|
||||
section: TeamSection,
|
||||
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,
|
||||
perform: () => (window.location.href = session.url),
|
||||
});
|
||||
@ -55,10 +68,9 @@ function getSessions(params?: { exclude?: string }) {
|
||||
return otherSessions;
|
||||
}
|
||||
|
||||
const Logo = styled("img")`
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
border-radius: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
export const rootTeamActions = [switchTeam, createTeam];
|
||||
|
@ -1,14 +1,21 @@
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import User from "~/models/User";
|
||||
import useBoolean from "~/hooks/useBoolean";
|
||||
import Initials from "./Initials";
|
||||
import placeholder from "./placeholder.png";
|
||||
|
||||
export interface IAvatar {
|
||||
avatarUrl: string | null;
|
||||
color: string;
|
||||
initial: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
size: number;
|
||||
src?: string;
|
||||
icon?: React.ReactNode;
|
||||
user?: User;
|
||||
model?: IAvatar;
|
||||
alt?: string;
|
||||
showBorder?: boolean;
|
||||
onClick?: React.MouseEventHandler<HTMLImageElement>;
|
||||
@ -16,20 +23,28 @@ type 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);
|
||||
|
||||
return (
|
||||
<AvatarWrapper>
|
||||
<CircleImg
|
||||
onError={handleError}
|
||||
src={error ? placeholder : src}
|
||||
$showBorder={showBorder}
|
||||
{...rest}
|
||||
/>
|
||||
<Relative>
|
||||
{src ? (
|
||||
<CircleImg
|
||||
onError={handleError}
|
||||
src={error ? placeholder : src}
|
||||
$showBorder={showBorder}
|
||||
{...rest}
|
||||
/>
|
||||
) : model ? (
|
||||
<Initials color={model.color} $showBorder={showBorder} {...rest}>
|
||||
{model.initial}
|
||||
</Initials>
|
||||
) : (
|
||||
<Initials $showBorder={showBorder} {...rest} />
|
||||
)}
|
||||
{icon && <IconWrapper>{icon}</IconWrapper>}
|
||||
</AvatarWrapper>
|
||||
</Relative>
|
||||
);
|
||||
}
|
||||
|
||||
@ -37,7 +52,7 @@ Avatar.defaultProps = {
|
||||
size: 24,
|
||||
};
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
const Relative = styled.div`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
|
@ -51,7 +51,7 @@ function AvatarWithPresence({
|
||||
$isObserving={isObserving}
|
||||
$color={user.color}
|
||||
>
|
||||
<Avatar src={user.avatarUrl} onClick={onClick} size={32} />
|
||||
<Avatar model={user} onClick={onClick} size={32} />
|
||||
</AvatarWrapper>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
27
app/components/Avatar/Initials.tsx
Normal file
27
app/components/Avatar/Initials.tsx
Normal 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;
|
@ -43,10 +43,10 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
<PaginatedList
|
||||
aria-label={t("Viewers")}
|
||||
items={users}
|
||||
renderItem={(item: User) => {
|
||||
const view = documentViews.find((v) => v.user.id === item.id);
|
||||
const isPresent = presentIds.includes(item.id);
|
||||
const isEditing = editingIds.includes(item.id);
|
||||
renderItem={(model: User) => {
|
||||
const view = documentViews.find((v) => v.user.id === model.id);
|
||||
const isPresent = presentIds.includes(model.id);
|
||||
const isEditing = editingIds.includes(model.id);
|
||||
const subtitle = isPresent
|
||||
? isEditing
|
||||
? t("Currently editing")
|
||||
@ -58,10 +58,10 @@ function DocumentViews({ document, isOpen }: Props) {
|
||||
});
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
title={item.name}
|
||||
key={model.id}
|
||||
title={model.name}
|
||||
subtitle={subtitle}
|
||||
image={<Avatar key={item.id} src={item.avatarUrl} size={32} />}
|
||||
image={<Avatar key={model.id} model={model} size={32} />}
|
||||
border={false}
|
||||
small
|
||||
/>
|
||||
|
@ -142,7 +142,7 @@ const EventListItem = ({ event, latest, document, ...rest }: Props) => {
|
||||
onClick={handleTimeClick}
|
||||
/>
|
||||
}
|
||||
image={<Avatar src={event.actor?.avatarUrl} size={32} />}
|
||||
image={<Avatar model={event.actor} size={32} />}
|
||||
subtitle={
|
||||
<Subtitle>
|
||||
{icon}
|
||||
|
@ -39,7 +39,7 @@ function Facepile({
|
||||
}
|
||||
|
||||
function DefaultAvatar(user: User) {
|
||||
return <Avatar user={user} src={user.avatarUrl} size={32} />;
|
||||
return <Avatar model={user} size={32} />;
|
||||
}
|
||||
|
||||
const AvatarWrapper = styled.div`
|
||||
|
@ -63,14 +63,7 @@ function AppSidebar() {
|
||||
<SidebarButton
|
||||
{...props}
|
||||
title={team.name}
|
||||
image={
|
||||
<StyledTeamLogo
|
||||
src={team.avatarUrl}
|
||||
width={32}
|
||||
height={32}
|
||||
alt={t("Logo")}
|
||||
/>
|
||||
}
|
||||
image={<TeamLogo model={team} size={32} alt={t("Logo")} />}
|
||||
showDisclosure
|
||||
/>
|
||||
)}
|
||||
@ -139,11 +132,6 @@ function AppSidebar() {
|
||||
);
|
||||
}
|
||||
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
margin-right: 4px;
|
||||
background: white;
|
||||
`;
|
||||
|
||||
const Drafts = styled(Text)`
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
@ -178,7 +178,7 @@ const Sidebar = React.forwardRef<HTMLDivElement, Props>(
|
||||
image={
|
||||
<StyledAvatar
|
||||
alt={user.name}
|
||||
src={user.avatarUrl}
|
||||
model={user}
|
||||
size={24}
|
||||
showBorder={false}
|
||||
/>
|
||||
|
@ -1,10 +1,7 @@
|
||||
import styled from "styled-components";
|
||||
import Avatar from "./Avatar";
|
||||
|
||||
const TeamLogo = styled.img<{ width?: number; height?: number; size?: string }>`
|
||||
width: ${(props) =>
|
||||
props.width ? `${props.width}px` : props.size || "auto"};
|
||||
height: ${(props) =>
|
||||
props.height ? `${props.height}px` : props.size || "38px"};
|
||||
const TeamLogo = styled(Avatar)`
|
||||
border-radius: 4px;
|
||||
border: 1px solid ${(props) => props.theme.divider};
|
||||
overflow: hidden;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { computed, observable } from "mobx";
|
||||
import { TeamPreference, TeamPreferences } from "@shared/types";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import BaseModel from "./BaseModel";
|
||||
import Field from "./decorators/Field";
|
||||
|
||||
@ -69,6 +70,16 @@ class Team extends BaseModel {
|
||||
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"
|
||||
* button rather than seamless always-editing.
|
||||
|
@ -40,6 +40,11 @@ class User extends ParanoidModel {
|
||||
|
||||
isSuspended: boolean;
|
||||
|
||||
@computed
|
||||
get initial(): string {
|
||||
return this.name ? this.name[0] : "?";
|
||||
}
|
||||
|
||||
@computed
|
||||
get isInvited(): boolean {
|
||||
return !this.lastActiveAt;
|
||||
|
@ -104,18 +104,16 @@ const MembershipPreview = ({ collection, limit = 8 }: Props) => {
|
||||
users={sortBy(collectionUsers, "lastActiveAt")}
|
||||
overflow={overflow}
|
||||
limit={limit}
|
||||
renderAvatar={(user) => (
|
||||
<StyledAvatar user={user} src={user.avatarUrl} size={32} />
|
||||
)}
|
||||
renderAvatar={(user) => <StyledAvatar model={user} size={32} />}
|
||||
/>
|
||||
</Fade>
|
||||
</NudeButton>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledAvatar = styled(Avatar)<{ user: User }>`
|
||||
const StyledAvatar = styled(Avatar)<{ model: User }>`
|
||||
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);
|
||||
|
@ -49,7 +49,7 @@ const MemberListItem = ({
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
actions={
|
||||
<Flex align="center" gap={8}>
|
||||
{onUpdate && (
|
||||
|
@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
|
@ -35,7 +35,7 @@ const GroupMemberListItem = ({ user, onRemove, onAdd }: Props) => {
|
||||
{user.isAdmin && <Badge primary={user.isAdmin}>{t("Admin")}</Badge>}
|
||||
</>
|
||||
}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
actions={
|
||||
<Flex align="center">
|
||||
{onRemove && <GroupMemberMenu onRemove={onRemove} />}
|
||||
|
@ -21,7 +21,7 @@ const UserListItem = ({ user, onAdd, canEdit }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={user.name}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.lastActiveAt ? (
|
||||
|
@ -169,7 +169,7 @@ function Login({ children }: Props) {
|
||||
/>
|
||||
<Logo>
|
||||
{config.logo ? (
|
||||
<TeamLogo width={48} height={48} src={config.logo} />
|
||||
<TeamLogo size={48} src={config.logo} />
|
||||
) : (
|
||||
<OutlineLogo size={42} fill="currentColor" />
|
||||
)}
|
||||
|
@ -26,7 +26,6 @@ function Details() {
|
||||
const form = useRef<HTMLFormElement>(null);
|
||||
const [name, setName] = useState(team.name);
|
||||
const [subdomain, setSubdomain] = useState(team.subdomain);
|
||||
const [avatarUrl, setAvatarUrl] = useState<string>(team.avatarUrl);
|
||||
const [defaultCollectionId, setDefaultCollectionId] = useState<string | null>(
|
||||
team.defaultCollectionId
|
||||
);
|
||||
@ -40,7 +39,6 @@ function Details() {
|
||||
try {
|
||||
await auth.updateTeam({
|
||||
name,
|
||||
avatarUrl,
|
||||
subdomain,
|
||||
defaultCollectionId,
|
||||
});
|
||||
@ -53,7 +51,7 @@ function Details() {
|
||||
});
|
||||
}
|
||||
},
|
||||
[auth, name, avatarUrl, subdomain, defaultCollectionId, showToast, t]
|
||||
[auth, name, subdomain, defaultCollectionId, showToast, t]
|
||||
);
|
||||
|
||||
const handleNameChange = React.useCallback(
|
||||
@ -71,7 +69,6 @@ function Details() {
|
||||
);
|
||||
|
||||
const handleAvatarUpload = async (avatarUrl: string) => {
|
||||
setAvatarUrl(avatarUrl);
|
||||
await auth.updateTeam({
|
||||
avatarUrl,
|
||||
});
|
||||
@ -115,7 +112,7 @@ function Details() {
|
||||
<ImageInput
|
||||
onSuccess={handleAvatarUpload}
|
||||
onError={handleAvatarError}
|
||||
src={avatarUrl}
|
||||
model={team}
|
||||
borderRadius={0}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
@ -18,7 +18,6 @@ const Profile = () => {
|
||||
const user = useCurrentUser();
|
||||
const form = React.useRef<HTMLFormElement>(null);
|
||||
const [name, setName] = React.useState<string>(user.name || "");
|
||||
const [avatarUrl, setAvatarUrl] = React.useState<string>(user.avatarUrl);
|
||||
const { showToast } = useToasts();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -28,7 +27,6 @@ const Profile = () => {
|
||||
try {
|
||||
await auth.updateUser({
|
||||
name,
|
||||
avatarUrl,
|
||||
});
|
||||
showToast(t("Profile saved"), {
|
||||
type: "success",
|
||||
@ -45,7 +43,6 @@ const Profile = () => {
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (avatarUrl: string) => {
|
||||
setAvatarUrl(avatarUrl);
|
||||
await auth.updateUser({
|
||||
avatarUrl,
|
||||
});
|
||||
@ -79,7 +76,7 @@ const Profile = () => {
|
||||
<ImageInput
|
||||
onSuccess={handleAvatarUpload}
|
||||
onError={handleAvatarError}
|
||||
src={avatarUrl}
|
||||
model={user}
|
||||
/>
|
||||
</SettingRow>
|
||||
<SettingRow
|
||||
|
@ -1,21 +1,22 @@
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import styled from "styled-components";
|
||||
import Avatar, { IAvatar } from "~/components/Avatar/Avatar";
|
||||
import Flex from "~/components/Flex";
|
||||
import ImageUpload, { Props as ImageUploadProps } from "./ImageUpload";
|
||||
|
||||
type Props = ImageUploadProps & {
|
||||
src?: string;
|
||||
model: IAvatar;
|
||||
};
|
||||
|
||||
export default function ImageInput({ src, ...rest }: Props) {
|
||||
export default function ImageInput({ model, ...rest }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<ImageBox>
|
||||
<ImageUpload {...rest}>
|
||||
<Avatar src={src} />
|
||||
<Flex auto align="center" justify="center">
|
||||
<StyledAvatar model={model} size={64} />
|
||||
<Flex auto align="center" justify="center" className="upload">
|
||||
{t("Upload")}
|
||||
</Flex>
|
||||
</ImageUpload>
|
||||
@ -28,8 +29,8 @@ const avatarStyles = `
|
||||
height: 64px;
|
||||
`;
|
||||
|
||||
const Avatar = styled.img`
|
||||
${avatarStyles};
|
||||
const StyledAvatar = styled(Avatar)`
|
||||
border-radius: 8px;
|
||||
`;
|
||||
|
||||
const ImageBox = styled(Flex)`
|
||||
@ -41,7 +42,7 @@ const ImageBox = styled(Flex)`
|
||||
background: ${(props) => props.theme.background};
|
||||
overflow: hidden;
|
||||
|
||||
div div {
|
||||
.upload {
|
||||
${avatarStyles};
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -53,7 +54,7 @@ const ImageBox = styled(Flex)`
|
||||
transition: all 250ms;
|
||||
}
|
||||
|
||||
&:hover div {
|
||||
&:hover .upload {
|
||||
opacity: 1;
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
color: ${(props) => props.theme.white};
|
||||
|
@ -29,7 +29,7 @@ function PeopleTable({ canManage, ...rest }: Props) {
|
||||
Cell: observer(
|
||||
({ value, row }: { value: string; row: { original: User } }) => (
|
||||
<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")})`}
|
||||
</Flex>
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ function SharesTable({ canManage, ...rest }: Props) {
|
||||
<Flex align="center" gap={4}>
|
||||
{row.original.createdBy && (
|
||||
<Avatar
|
||||
src={row.original.createdBy.avatarUrl}
|
||||
model={row.original.createdBy}
|
||||
alt={row.original.createdBy.name}
|
||||
/>
|
||||
)}
|
||||
|
@ -20,7 +20,7 @@ const UserListItem = ({ user, showMenu }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
title={<Title>{user.name}</Title>}
|
||||
image={<Avatar src={user.avatarUrl} size={32} />}
|
||||
image={<Avatar model={user} size={32} />}
|
||||
subtitle={
|
||||
<>
|
||||
{user.email ? `${user.email} · ` : undefined}
|
||||
|
@ -39,7 +39,7 @@ function UserProfile(props: Props) {
|
||||
<Modal
|
||||
title={
|
||||
<Flex align="center">
|
||||
<Avatar src={user.avatarUrl} size={38} alt={t("Profile picture")} />
|
||||
<Avatar model={user} size={38} alt={t("Profile picture")} />
|
||||
<span> {user.name}</span>
|
||||
</Flex>
|
||||
}
|
||||
|
@ -226,14 +226,17 @@ export default class AuthStore {
|
||||
preferences?: UserPreferences;
|
||||
}) => {
|
||||
this.isSaving = true;
|
||||
const previousData = this.user?.toAPI();
|
||||
|
||||
try {
|
||||
this.user?.updateFromJson(params);
|
||||
const res = await client.post(`/users.update`, params);
|
||||
invariant(res?.data, "User response not available");
|
||||
runInAction("AuthStore#updateUser", () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.user = new User(res.data, this);
|
||||
});
|
||||
this.user?.updateFromJson(res.data);
|
||||
this.addPolicies(res.policies);
|
||||
} catch (err) {
|
||||
this.user?.updateFromJson(previousData);
|
||||
throw err;
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
@ -251,14 +254,17 @@ export default class AuthStore {
|
||||
preferences?: TeamPreferences;
|
||||
}) => {
|
||||
this.isSaving = true;
|
||||
const previousData = this.team?.toAPI();
|
||||
|
||||
try {
|
||||
this.team?.updateFromJson(params);
|
||||
const res = await client.post(`/team.update`, params);
|
||||
invariant(res?.data, "Team response not available");
|
||||
runInAction("AuthStore#updateTeam", () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.team = new Team(res.data, this);
|
||||
});
|
||||
this.team?.updateFromJson(res.data);
|
||||
this.addPolicies(res.policies);
|
||||
} catch (err) {
|
||||
this.team?.updateFromJson(previousData);
|
||||
throw err;
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ async function teamCreator({
|
||||
// one via ClearBit, or fallback to colored initials in worst case scenario
|
||||
if (!avatarUrl || !avatarUrl.startsWith("http")) {
|
||||
avatarUrl = await generateAvatarUrl({
|
||||
name,
|
||||
domain,
|
||||
id: subdomain,
|
||||
});
|
||||
|
@ -329,13 +329,6 @@ export class Environment {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
|
@ -24,7 +24,6 @@ import { CollectionPermission, TeamPreference } from "@shared/types";
|
||||
import { getBaseDomain, RESERVED_SUBDOMAINS } from "@shared/utils/domains";
|
||||
import env from "@server/env";
|
||||
import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
|
||||
import { generateAvatarUrl } from "@server/utils/avatars";
|
||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||
import Attachment from "./Attachment";
|
||||
import AuthenticationProvider from "./AuthenticationProvider";
|
||||
@ -94,8 +93,20 @@ class Team extends ParanoidModel {
|
||||
@AllowNull
|
||||
@IsUrl
|
||||
@Length({ max: 4096, msg: "avatarUrl must be 4096 characters or less" })
|
||||
@Column
|
||||
avatarUrl: string | null;
|
||||
@Column(DataType.STRING)
|
||||
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)
|
||||
@Column
|
||||
@ -163,16 +174,6 @@ class Team extends ParanoidModel {
|
||||
return url.href.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
get logoUrl() {
|
||||
return (
|
||||
this.avatarUrl ||
|
||||
generateAvatarUrl({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preferences that decide behavior for the team.
|
||||
*
|
||||
|
@ -180,17 +180,11 @@ class User extends ParanoidModel {
|
||||
get avatarUrl() {
|
||||
const original = this.getDataValue("avatarUrl");
|
||||
|
||||
if (original) {
|
||||
if (original && !original.startsWith("https://tiley.herokuapp.com")) {
|
||||
return original;
|
||||
}
|
||||
|
||||
const color = this.color.replace(/^#/, "");
|
||||
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}`;
|
||||
return null;
|
||||
}
|
||||
|
||||
set avatarUrl(value: string | null) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
exports[`presents a user 1`] = `
|
||||
Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80",
|
||||
"avatarUrl": null,
|
||||
"color": "#FF5C80",
|
||||
"createdAt": undefined,
|
||||
"id": "123",
|
||||
@ -17,7 +17,7 @@ Object {
|
||||
|
||||
exports[`presents a user without slack data 1`] = `
|
||||
Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/d41d8cd98f00b204e9800998ecf8427e/T.png?c=FF5C80",
|
||||
"avatarUrl": null,
|
||||
"color": "#FF5C80",
|
||||
"createdAt": undefined,
|
||||
"id": "123",
|
||||
|
@ -4,7 +4,7 @@ export default function present(team: Team, isSignedIn = false) {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
avatarUrl: team.logoUrl,
|
||||
avatarUrl: team.avatarUrl,
|
||||
url: team.url,
|
||||
isSignedIn,
|
||||
};
|
||||
|
@ -4,7 +4,7 @@ export default function present(team: Team) {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
avatarUrl: team.logoUrl,
|
||||
avatarUrl: team.avatarUrl,
|
||||
sharing: team.sharing,
|
||||
memberCollectionCreate: team.memberCollectionCreate,
|
||||
collaborativeEditing: team.collaborativeEditing,
|
||||
|
@ -3,7 +3,7 @@
|
||||
exports[`#users.activate should activate a suspended user 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
@ -59,7 +59,7 @@ Object {
|
||||
exports[`#users.demote should demote an admin 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
@ -97,7 +97,7 @@ Object {
|
||||
exports[`#users.demote should demote an admin to member 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
@ -135,7 +135,7 @@ Object {
|
||||
exports[`#users.demote should demote an admin to viewer 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
@ -191,7 +191,7 @@ Object {
|
||||
exports[`#users.promote should promote a new admin 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
@ -256,7 +256,7 @@ Object {
|
||||
exports[`#users.suspend should suspend an user 1`] = `
|
||||
Object {
|
||||
"data": Object {
|
||||
"avatarUrl": "https://tiley.herokuapp.com/avatar/111d68d06e2d317b5a59c2c6c5bad808/U.png?c=e600e0",
|
||||
"avatarUrl": null,
|
||||
"color": "#e600e0",
|
||||
"createdAt": "2018-01-02T00:00:00.000Z",
|
||||
"email": "user1@example.com",
|
||||
|
@ -93,7 +93,7 @@ export async function signIn(
|
||||
...existing,
|
||||
[team.id]: {
|
||||
name: team.name,
|
||||
logoUrl: team.logoUrl,
|
||||
logoUrl: team.avatarUrl,
|
||||
url: team.url,
|
||||
},
|
||||
})
|
||||
|
@ -4,43 +4,6 @@ it("should return clearbit url if available", async () => {
|
||||
const url = await generateAvatarUrl({
|
||||
id: "google",
|
||||
domain: "google.com",
|
||||
name: "Google",
|
||||
});
|
||||
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"
|
||||
);
|
||||
});
|
||||
|
@ -1,21 +1,17 @@
|
||||
import crypto from "crypto";
|
||||
import fetch from "fetch-with-proxy";
|
||||
import env from "@server/env";
|
||||
|
||||
export async function generateAvatarUrl({
|
||||
id,
|
||||
domain,
|
||||
name = "Unknown",
|
||||
}: {
|
||||
id: string;
|
||||
domain?: string;
|
||||
name?: string;
|
||||
}) {
|
||||
// attempt to get logo from Clearbit API. If one doesn't exist then
|
||||
// fall back to using tiley to generate a placeholder logo
|
||||
const hash = crypto.createHash("sha256");
|
||||
hash.update(id);
|
||||
const hashedId = hash.digest("hex");
|
||||
let cbResponse, cbUrl;
|
||||
|
||||
if (domain) {
|
||||
@ -28,8 +24,5 @@ export async function generateAvatarUrl({
|
||||
}
|
||||
}
|
||||
|
||||
const tileyUrl = `${
|
||||
env.DEFAULT_AVATAR_HOST
|
||||
}/avatar/${hashedId}/${encodeURIComponent(name[0])}.png`;
|
||||
return cbUrl && cbResponse && cbResponse.status === 200 ? cbUrl : tileyUrl;
|
||||
return cbUrl && cbResponse && cbResponse.status === 200 ? cbUrl : null;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import fetch from "fetch-with-proxy";
|
||||
import { compact } from "lodash";
|
||||
import { useAgent } from "request-filtering-agent";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import env from "@server/env";
|
||||
import Logger from "@server/logging/Logger";
|
||||
|
||||
const AWS_S3_ACCELERATE_URL = process.env.AWS_S3_ACCELERATE_URL;
|
||||
@ -184,11 +183,7 @@ export const uploadToS3FromUrl = async (
|
||||
acl: string
|
||||
) => {
|
||||
const endpoint = publicS3Endpoint(true);
|
||||
if (
|
||||
url.startsWith("/api") ||
|
||||
url.startsWith(endpoint) ||
|
||||
url.startsWith(env.DEFAULT_AVATAR_HOST)
|
||||
) {
|
||||
if (url.startsWith("/api") || url.startsWith(endpoint)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user