diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index f2ca22cd..8e87ae28 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({ /> ))} 0)} contrast={contrast} sending={typeof queued !== "undefined"} diff --git a/src/components/common/messaging/MessageBase.tsx b/src/components/common/messaging/MessageBase.tsx index e040168e..56b82e7e 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` display: flex; overflow: none; @@ -70,6 +77,14 @@ export default styled.div` 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 c0822924..61929399 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 ( - + { !hideInfo && - + } {children} ); diff --git a/src/components/common/messaging/attachments/MessageReply.tsx b/src/components/common/messaging/attachments/MessageReply.tsx index 6fef2bf7..435f2d54 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; + } - svg:first-child { + .content { + gap: 4px; + display: flex; + cursor: pointer; + align-items: center; + flex-direction: row; + + > * { + pointer-events: none; + } + } + + > 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(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 ( @@ -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 ( - - {message.attachments && message.attachments.length > 0 && ( - - )} - {message.author === SYSTEM_USER_ID ? ( - - ) : ( - - )} + { user?.relationship === Users.Relationship.Blocked ? + <>Blocked User : + <> + {message.author === SYSTEM_USER_ID ? ( + + ) : <> + +
{ + 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 && ( + + )} + +
+ } + + }
); } diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts index 83bb2efe..be9829d1 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 31cb705e..95b964b0 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 5f5f439e..a5da03be 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(undefined); // ? This is the scroll container. const ref = useRef(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) { )} {state.type === "RENDER" && ( - + )} {state.type === "EMPTY" && } diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index d573578b..7201da1d 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} />, ); }