mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-06 17:11:55 +00:00
Work on channels, render content of messages.
This commit is contained in:
@@ -32,12 +32,6 @@ export default function ChannelIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLI
|
||||
height={size}
|
||||
aria-hidden="true"
|
||||
square={isServerChannel}
|
||||
src={iconURL ?? fallback}
|
||||
onError={ e => {
|
||||
let el = e.currentTarget;
|
||||
if (el.src !== fallback) {
|
||||
el.src = fallback
|
||||
}
|
||||
}} />
|
||||
src={iconURL ?? fallback} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -42,12 +42,6 @@ export default function ServerIcon(props: Props & Omit<JSX.HTMLAttributes<HTMLIm
|
||||
width={size}
|
||||
height={size}
|
||||
aria-hidden="true"
|
||||
src={iconURL}
|
||||
onError={ e => {
|
||||
let el = e.currentTarget;
|
||||
if (el.src !== fallback) {
|
||||
el.src = fallback
|
||||
}
|
||||
}} />
|
||||
src={iconURL} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGEle
|
||||
|
||||
const { target, attachment, size, voice, status, animate, children, as, ...svgProps } = props;
|
||||
const iconURL = client.generateFileURL(target?.avatar ?? attachment, { max_side: 256 }, animate)
|
||||
?? (target && client.users.getDefaultAvatarURL(target._id));
|
||||
?? (target ? client.users.getDefaultAvatarURL(target._id) : fallback);
|
||||
|
||||
return (
|
||||
<IconBase {...svgProps}
|
||||
@@ -65,13 +65,7 @@ export default function UserIcon(props: Props & Omit<JSX.SVGAttributes<SVGSVGEle
|
||||
<foreignObject x="0" y="0" width="32" height="32">
|
||||
{
|
||||
<img src={iconURL}
|
||||
draggable={false}
|
||||
onError={ e => {
|
||||
let el = e.currentTarget;
|
||||
if (el.src !== fallback) {
|
||||
el.src = fallback
|
||||
}
|
||||
}} />
|
||||
draggable={false} />
|
||||
}
|
||||
</foreignObject>
|
||||
{props.status && (
|
||||
|
||||
14
src/components/common/UserShort.tsx
Normal file
14
src/components/common/UserShort.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { User } from "revolt.js";
|
||||
import UserIcon from "./UserIcon";
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
export function Username({ user }: { user?: User }) {
|
||||
return <b>{ user?.username ?? <Text id="app.main.channel.unknown_user" /> }</b>;
|
||||
}
|
||||
|
||||
export default function UserShort({ user }: { user?: User }) {
|
||||
return <>
|
||||
<UserIcon size={24} target={user} />
|
||||
<Username user={user} />
|
||||
</>;
|
||||
}
|
||||
37
src/components/common/messaging/Message.tsx
Normal file
37
src/components/common/messaging/Message.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import UserIcon from "../UserIcon";
|
||||
import { Username } from "../UserShort";
|
||||
import Markdown from "../../markdown/Markdown";
|
||||
import { Children } from "../../../types/Preact";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { useUser } from "../../../context/revoltjs/hooks";
|
||||
import { MessageObject } from "../../../context/revoltjs/util";
|
||||
import MessageBase, { MessageContent, MessageDetail, MessageInfo } from "./MessageBase";
|
||||
|
||||
interface Props {
|
||||
attachContext?: boolean
|
||||
message: MessageObject
|
||||
contrast?: boolean
|
||||
content?: Children
|
||||
head?: boolean
|
||||
}
|
||||
|
||||
export default function Message({ attachContext, message, contrast, content, head }: Props) {
|
||||
// TODO: Can improve re-renders here by providing a list
|
||||
// TODO: of dependencies. We only need to update on u/avatar.
|
||||
let user = useUser(message.author);
|
||||
|
||||
return (
|
||||
<MessageBase contrast={contrast}
|
||||
onContextMenu={attachContext ? attachContextMenu('Menu', { message, contextualChannel: message.channel }) : undefined}>
|
||||
<MessageInfo>
|
||||
{ head ?
|
||||
<UserIcon target={user} size={36} /> :
|
||||
<MessageDetail message={message} /> }
|
||||
</MessageInfo>
|
||||
<MessageContent>
|
||||
{ head && <Username user={user} /> }
|
||||
{ content ?? <Markdown content={message.content as string} /> }
|
||||
</MessageContent>
|
||||
</MessageBase>
|
||||
)
|
||||
}
|
||||
111
src/components/common/messaging/MessageBase.tsx
Normal file
111
src/components/common/messaging/MessageBase.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import dayjs from "dayjs";
|
||||
import styled, { css } from "styled-components";
|
||||
import { decodeTime } from "ulid";
|
||||
import { MessageObject } from "../../../context/revoltjs/util";
|
||||
|
||||
export interface BaseMessageProps {
|
||||
head?: boolean,
|
||||
status?: boolean,
|
||||
mention?: boolean,
|
||||
blocked?: boolean,
|
||||
sending?: boolean,
|
||||
contrast?: boolean
|
||||
}
|
||||
|
||||
export default styled.div<BaseMessageProps>`
|
||||
display: flex;
|
||||
overflow-x: none;
|
||||
padding: .125rem;
|
||||
flex-direction: row;
|
||||
padding-right: 16px;
|
||||
|
||||
${ props => props.contrast && css`
|
||||
padding: .3rem;
|
||||
border-radius: 4px;
|
||||
background: var(--hover);
|
||||
` }
|
||||
|
||||
${ props => props.head && css`
|
||||
margin-top: 12px;
|
||||
` }
|
||||
|
||||
${ props => props.mention && css`
|
||||
background: var(--mention);
|
||||
` }
|
||||
|
||||
${ props => props.blocked && css`
|
||||
filter: blur(4px);
|
||||
transition: 0.2s ease filter;
|
||||
|
||||
&:hover {
|
||||
filter: none;
|
||||
}
|
||||
` }
|
||||
|
||||
${ props => props.sending && css`
|
||||
opacity: 0.8;
|
||||
color: var(--tertiary-foreground);
|
||||
` }
|
||||
|
||||
${ props => props.status && css`
|
||||
color: var(--error);
|
||||
` }
|
||||
|
||||
.copy {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--hover);
|
||||
|
||||
time {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const MessageInfo = styled.div`
|
||||
width: 62px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
padding-top: 2px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
::selection {
|
||||
background-color: transparent;
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
|
||||
time {
|
||||
opacity: 0;
|
||||
cursor: default;
|
||||
display: inline;
|
||||
font-size: 10px;
|
||||
padding-top: 1px;
|
||||
color: var(--tertiary-foreground);
|
||||
}
|
||||
`;
|
||||
|
||||
export const MessageContent = styled.div`
|
||||
min-width: 0;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
font-size: 0.875rem;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export function MessageDetail({ message }: { message: MessageObject }) {
|
||||
return (
|
||||
<>
|
||||
<time>
|
||||
<i className="copy">[</i>
|
||||
{dayjs(decodeTime(message._id)).format("H:mm")}
|
||||
<i className="copy">]</i>
|
||||
</time>
|
||||
</>
|
||||
)
|
||||
}
|
||||
152
src/components/common/messaging/SystemMessage.tsx
Normal file
152
src/components/common/messaging/SystemMessage.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { User } from "revolt.js";
|
||||
import classNames from "classnames";
|
||||
import { attachContextMenu } from "preact-context-menu";
|
||||
import { MessageObject } from "../../../context/revoltjs/util";
|
||||
import { useForceUpdate, useUser } from "../../../context/revoltjs/hooks";
|
||||
import { TextReact } from "../../../lib/i18n";
|
||||
import UserIcon from "../UserIcon";
|
||||
import Username from "../UserShort";
|
||||
import UserShort from "../UserShort";
|
||||
import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase";
|
||||
import styled from "styled-components";
|
||||
|
||||
const SystemContent = styled.div`
|
||||
gap: 4px;
|
||||
display: flex;
|
||||
padding: 2px 0;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
type SystemMessageParsed =
|
||||
| { type: "text"; content: string }
|
||||
| { type: "user_added"; user: User; by: User }
|
||||
| { type: "user_remove"; user: User; by: User }
|
||||
| { type: "user_joined"; user: User }
|
||||
| { type: "user_left"; user: User }
|
||||
| { type: "user_kicked"; user: User }
|
||||
| { type: "user_banned"; user: User }
|
||||
| { type: "channel_renamed"; name: string; by: User }
|
||||
| { type: "channel_description_changed"; by: User }
|
||||
| { type: "channel_icon_changed"; by: User };
|
||||
|
||||
interface Props {
|
||||
attachContext?: boolean;
|
||||
message: MessageObject;
|
||||
}
|
||||
|
||||
export function SystemMessage({ attachContext, message }: Props) {
|
||||
const ctx = useForceUpdate();
|
||||
|
||||
let data: SystemMessageParsed;
|
||||
let content = message.content;
|
||||
if (typeof content === "object") {
|
||||
switch (content.type) {
|
||||
case "text":
|
||||
data = content;
|
||||
break;
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
data = {
|
||||
type: content.type,
|
||||
user: useUser(content.id, ctx) as User,
|
||||
by: useUser(content.by, ctx) as User
|
||||
};
|
||||
break;
|
||||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
data = {
|
||||
type: content.type,
|
||||
user: useUser(content.id, ctx) as User
|
||||
};
|
||||
break;
|
||||
case "channel_renamed":
|
||||
data = {
|
||||
type: "channel_renamed",
|
||||
name: content.name,
|
||||
by: useUser(content.by, ctx) as User
|
||||
};
|
||||
break;
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
data = {
|
||||
type: content.type,
|
||||
by: useUser(content.by, ctx) as User
|
||||
};
|
||||
break;
|
||||
default:
|
||||
data = { type: "text", content: JSON.stringify(content) };
|
||||
}
|
||||
} else {
|
||||
data = { type: "text", content };
|
||||
}
|
||||
|
||||
let children;
|
||||
switch (data.type) {
|
||||
case "text":
|
||||
children = <span>{data.content}</span>;
|
||||
break;
|
||||
case "user_added":
|
||||
case "user_remove":
|
||||
children = (
|
||||
<TextReact
|
||||
id={`app.main.channel.system.${data.type === 'user_added' ? "added_by" : "removed_by"}`}
|
||||
fields={{
|
||||
user: <UserShort user={data.user} />,
|
||||
other_user: <UserShort user={data.by} />
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "user_joined":
|
||||
case "user_left":
|
||||
case "user_kicked":
|
||||
case "user_banned":
|
||||
children = (
|
||||
<TextReact
|
||||
id={`app.main.channel.system.${data.type}`}
|
||||
fields={{
|
||||
user: <UserShort user={data.user} />
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "channel_renamed":
|
||||
children = (
|
||||
<TextReact
|
||||
id={`app.main.channel.system.channel_renamed`}
|
||||
fields={{
|
||||
user: <UserShort user={data.by} />,
|
||||
name: <b>{data.name}</b>
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case "channel_description_changed":
|
||||
case "channel_icon_changed":
|
||||
children = (
|
||||
<TextReact
|
||||
id={`app.main.channel.system.${data.type}`}
|
||||
fields={{
|
||||
user: <UserShort user={data.by} />
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageBase
|
||||
onContextMenu={attachContext ? attachContextMenu('Menu',
|
||||
{ message, contextualChannel: message.channel }
|
||||
) : undefined}>
|
||||
<MessageInfo>
|
||||
<MessageDetail message={message} />
|
||||
</MessageInfo>
|
||||
<SystemContent>{children}</SystemContent>
|
||||
</MessageBase>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export interface MarkdownProps {
|
||||
export default function Markdown(props: MarkdownProps) {
|
||||
return (
|
||||
// @ts-expect-error
|
||||
<Suspense fallback="Getting ready to render Markdown...">
|
||||
<Suspense fallback={props.content}>
|
||||
<Renderer {...props} />
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
48
src/components/ui/DateDivider.tsx
Normal file
48
src/components/ui/DateDivider.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import dayjs from "dayjs";
|
||||
import styled, { css } from "styled-components";
|
||||
|
||||
const Base = styled.div<{ unread?: boolean }>`
|
||||
height: 0;
|
||||
display: flex;
|
||||
margin: 14px 10px;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
border-top: thin solid var(--tertiary-foreground);
|
||||
|
||||
time {
|
||||
margin-top: -2px;
|
||||
font-size: .6875rem;
|
||||
line-height: .6875rem;
|
||||
padding: 2px 5px 2px 0;
|
||||
color: var(--tertiary-foreground);
|
||||
background: var(--primary-background);
|
||||
}
|
||||
|
||||
${ props => props.unread && css`
|
||||
border-top: thin solid var(--accent);
|
||||
` }
|
||||
`;
|
||||
|
||||
const Unread = styled.div`
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
padding: 5px 8px;
|
||||
border-radius: 60px;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
date: Date;
|
||||
unread?: boolean;
|
||||
}
|
||||
|
||||
export default function DateDivider(props: Props) {
|
||||
return (
|
||||
<Base unread={props.unread}>
|
||||
{ props.unread && <Unread>NEW</Unread> }
|
||||
<time>
|
||||
{ dayjs(props.date).format("LL") }
|
||||
</time>
|
||||
</Base>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user