diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index f2ca22cd9ba30917aa51eb88b0536d94f748798d..8e87ae282fdb2a77be755f4ae27786409f8ff2c3 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -28,12 +28,14 @@ interface Props { attachContext?: boolean; queued?: QueuedMessage; message: MessageObject; + highlight?: boolean; contrast?: boolean; content?: Children; head?: boolean; } function Message({ + highlight, attachContext, message, contrast, @@ -72,6 +74,7 @@ function Message({ /> ))} <MessageBase + highlight={highlight} head={head && !(message.replies && message.replies.length > 0)} contrast={contrast} sending={typeof queued !== "undefined"} diff --git a/src/components/common/messaging/MessageBase.tsx b/src/components/common/messaging/MessageBase.tsx index e040168ef1043971b8ab184a41b0c1c3738561f3..56b82e7edc6d436261cdfcbaecdaff4dcb1281df 100644 --- a/src/components/common/messaging/MessageBase.tsx +++ b/src/components/common/messaging/MessageBase.tsx @@ -1,4 +1,4 @@ -import styled, { css } from "styled-components"; +import styled, { css, keyframes } from "styled-components"; import { decodeTime } from "ulid"; import { Text } from "preact-i18n"; @@ -17,8 +17,15 @@ export interface BaseMessageProps { blocked?: boolean; sending?: boolean; contrast?: boolean; + highlight?: boolean; } +const highlight = keyframes` + 0% { background: var(--mention); } + 66% { background: var(--mention); } + 100% { background: transparent; } +`; + export default styled.div<BaseMessageProps>` display: flex; overflow: none; @@ -70,6 +77,14 @@ export default styled.div<BaseMessageProps>` color: var(--error); `} + ${(props) => + props.highlight && + css` + animation-name: ${highlight}; + animation-timing-function: ease; + animation-duration: 3s; + `} + .detail { gap: 8px; display: flex; diff --git a/src/components/common/messaging/SystemMessage.tsx b/src/components/common/messaging/SystemMessage.tsx index c08229249b504b6ef8ce1e9273a4961b55a3bd4c..6192939932be82fc75de224c05536f365aaa3017 100644 --- a/src/components/common/messaging/SystemMessage.tsx +++ b/src/components/common/messaging/SystemMessage.tsx @@ -35,9 +35,11 @@ type SystemMessageParsed = interface Props { attachContext?: boolean; message: MessageObject; + highlight?: boolean; + hideInfo?: boolean; } -export function SystemMessage({ attachContext, message }: Props) { +export function SystemMessage({ attachContext, message, highlight, hideInfo }: Props) { const ctx = useForceUpdate(); let data: SystemMessageParsed; @@ -143,6 +145,7 @@ export function SystemMessage({ attachContext, message }: Props) { return ( <MessageBase + highlight={highlight} onContextMenu={ attachContext ? attachContextMenu("Menu", { @@ -151,9 +154,9 @@ export function SystemMessage({ attachContext, message }: Props) { }) : undefined }> - <MessageInfo> + { !hideInfo && <MessageInfo> <MessageDetail message={message} position="left" /> - </MessageInfo> + </MessageInfo> } <SystemContent>{children}</SystemContent> </MessageBase> ); diff --git a/src/components/common/messaging/attachments/MessageReply.tsx b/src/components/common/messaging/attachments/MessageReply.tsx index 6fef2bf7429c44668fe9fe488255e392b54b8cc8..435f2d544b1ff937892ee0f86ce9ba54b3f2f5fb 100644 --- a/src/components/common/messaging/attachments/MessageReply.tsx +++ b/src/components/common/messaging/attachments/MessageReply.tsx @@ -6,11 +6,15 @@ import { Text } from "preact-i18n"; import { useRenderState } from "../../../../lib/renderer/Singleton"; -import { useUser } from "../../../../context/revoltjs/hooks"; +import { useForceUpdate, useUser } from "../../../../context/revoltjs/hooks"; import Markdown from "../../../markdown/Markdown"; import UserShort from "../../user/UserShort"; import { SystemMessage } from "../SystemMessage"; +import { Users } from "revolt.js/dist/api/objects"; +import { useHistory } from "react-router-dom"; +import { useEffect, useLayoutEffect, useState } from "preact/hooks"; +import { mapMessage, MessageObject } from "../../../../context/revoltjs/util"; interface Props { channel: string; @@ -25,18 +29,32 @@ export const ReplyBase = styled.div<{ }>` gap: 4px; display: flex; + margin: 0 30px; font-size: 0.8em; - margin-left: 30px; user-select: none; margin-bottom: 4px; align-items: center; color: var(--secondary-foreground); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; + * { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .content { + gap: 4px; + display: flex; + cursor: pointer; + align-items: center; + flex-direction: row; + + > * { + pointer-events: none; + } + } - svg:first-child { + > svg:first-child { flex-shrink: 0; transform: scaleX(-1); color: var(--tertiary-foreground); @@ -62,10 +80,23 @@ export const ReplyBase = styled.div<{ `; export function MessageReply({ index, channel, id }: Props) { + const ctx = useForceUpdate(); const view = useRenderState(channel); if (view?.type !== "RENDER") return null; - const message = view.messages.find((x) => x._id === id); + const [ message, setMessage ] = useState<MessageObject | undefined>(undefined); + useLayoutEffect(() => { + // ! FIXME: We should do this through the message renderer, so it can fetch it from cache if applicable. + const m = view.messages.find((x) => x._id === id); + + if (m) { + setMessage(m); + } else { + ctx.client.channels.fetchMessage(channel, id) + .then(m => setMessage(mapMessage(m))); + } + }, [ view.messages ]); + if (!message) { return ( <ReplyBase head={index === 0} fail> @@ -77,23 +108,38 @@ export function MessageReply({ index, channel, id }: Props) { ); } - const user = useUser(message.author); + const user = useUser(message.author, ctx); + const history = useHistory(); return ( <ReplyBase head={index === 0}> <Reply size={16} /> - <UserShort user={user} size={16} /> - {message.attachments && message.attachments.length > 0 && ( - <File size={16} /> - )} - {message.author === SYSTEM_USER_ID ? ( - <SystemMessage message={message} /> - ) : ( - <Markdown - disallowBigEmoji - content={(message.content as string).replace(/\n/g, " ")} - /> - )} + { user?.relationship === Users.Relationship.Blocked ? + <>Blocked User</> : + <> + {message.author === SYSTEM_USER_ID ? ( + <SystemMessage message={message} hideInfo /> + ) : <> + <UserShort user={user} size={16} /> + <div className="content" onClick={() => { + let obj = ctx.client.channels.get(channel); + if (obj?.channel_type === 'TextChannel') { + history.push(`/server/${obj.server}/channel/${obj._id}/${message._id}`); + } else { + history.push(`/channel/${channel}/${message._id}`); + } + }}> + {message.attachments && message.attachments.length > 0 && ( + <File size={16} /> + )} + <Markdown + disallowBigEmoji + content={(message.content as string).replace(/\n/g, " ")} + /> + </div> + </>} + </> + } </ReplyBase> ); } diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts index 83bb2efe0f90feed338ebf204f113abefa11e377..be9829d1228a45abd4f6ab02c34a5bc6d7fd46c7 100644 --- a/src/lib/renderer/Singleton.ts +++ b/src/lib/renderer/Singleton.ts @@ -73,6 +73,16 @@ export class SingletonRenderer extends EventEmitter3 { } async init(id: string, message_id?: string) { + if (message_id) { + if (this.state.type === 'RENDER') { + let message = this.state.messages.find(x => x._id === message_id); + if (message) { + this.emit("scroll", { type: "ScrollToView", id: message_id }); + return; + } + } + } + this.channel = id; this.stale = false; this.setStateUnguarded({ type: "LOADING" }); diff --git a/src/pages/channels/ChannelHeader.tsx b/src/pages/channels/ChannelHeader.tsx index 31cb705efc50e169282017f32328961b32e7f786..95b964b049555be24773b15162fe49f7521b8448 100644 --- a/src/pages/channels/ChannelHeader.tsx +++ b/src/pages/channels/ChannelHeader.tsx @@ -58,6 +58,10 @@ const Info = styled.div` font-size: 0.8em; font-weight: 400; color: var(--secondary-foreground); + + > * { + pointer-events: none; + } } `; diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index 5f5f439e5df65cddb20b4eecd556ecf6fc18df35..a5da03be07714cc6fa74e8b8181919ace957f84b 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -60,7 +60,9 @@ export function MessageArea({ id }: Props) { const status = useContext(StatusContext); const { focusTaken } = useContext(IntermediateContext); + // ? Required data for message links. const { message } = useParams<{ message: string }>(); + const [highlight, setHighlight] = useState<string | undefined>(undefined); // ? This is the scroll container. const ref = useRef<HTMLDivElement>(null); @@ -99,7 +101,7 @@ export function MessageArea({ id }: Props) { }); } else if (scrollState.current.type === "ScrollToView") { document.getElementById(scrollState.current.id) - ?.scrollIntoView(); + ?.scrollIntoView({ block: 'center' }); setScrollState({ type: "Free" }); } else if (scrollState.current.type === "OffsetTop") { @@ -170,6 +172,7 @@ export function MessageArea({ id }: Props) { // ? If message present or changes, load it as well. useEffect(() => { if (message) { + setHighlight(message); SingletonMessageRenderer.init(id, message); let channel = client.channels.get(id); @@ -284,7 +287,7 @@ export function MessageArea({ id }: Props) { </RequiresOnline> )} {state.type === "RENDER" && ( - <MessageRenderer id={id} state={state} /> + <MessageRenderer id={id} state={state} highlight={highlight} /> )} {state.type === "EMPTY" && <ConversationStart id={id} />} </div> diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index d573578b29c2ed560960845a1c4ed1ec8c02ebdc..7201da1d4680b3a1d23c8e42f7470a6a2e399d19 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -28,6 +28,7 @@ import MessageEditor from "./MessageEditor"; interface Props { id: string; state: RenderState; + highlight?: string; queue: QueuedMessage[]; } @@ -42,7 +43,7 @@ const BlockedMessage = styled.div` } `; -function MessageRenderer({ id, state, queue }: Props) { +function MessageRenderer({ id, state, queue, highlight }: Props) { if (state.type !== "RENDER") return null; const client = useContext(AppContext); @@ -132,6 +133,7 @@ function MessageRenderer({ id, state, queue }: Props) { key={message._id} message={message} attachContext + highlight={highlight === message._id} />, ); } else { @@ -158,6 +160,7 @@ function MessageRenderer({ id, state, queue }: Props) { ) : undefined } attachContext + highlight={highlight === message._id} />, ); }