diff --git a/.gitmodules b/.gitmodules index e115ad5f..ef120e17 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,10 +1,12 @@ [submodule "external/lang"] path = external/lang - url = https://github.com/revoltchat/translations + url = https://github.com/archem-team/translations + branch = revite-backports [submodule "external/components"] path = external/components url = https://github.com/archem-team/components branch = bug/deleted_account_notif [submodule "external/revolt.js"] path = external/revolt.js - url = https://github.com/revoltchat/revolt.js + url = https://github.com/archem-team/revolt.js + branch = pin_message diff --git a/.vscode/settings.json b/.vscode/settings.json index 324a9612..50975ba9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true + "editor.formatOnSave": true, + "[typescriptreact]": { + "editor.defaultFormatter": "vscode.typescript-language-features" + } } diff --git a/external/lang b/external/lang index 3195d642..8ecb9a34 160000 --- a/external/lang +++ b/external/lang @@ -1 +1 @@ -Subproject commit 3195d642cd766cb62d34eb2a57ce3a09e775e91f +Subproject commit 8ecb9a34b1b459b5280a6351a4044dfa44b68019 diff --git a/external/revolt.js b/external/revolt.js index cd9e84a3..00770257 160000 --- a/external/revolt.js +++ b/external/revolt.js @@ -1 +1 @@ -Subproject commit cd9e84a337c72709b82bb4eca794ec7474a0ee7e +Subproject commit 007702579cd6e611fce79498461e89951387108d diff --git a/src/components/common/messaging/MessageBox.tsx b/src/components/common/messaging/MessageBox.tsx index e2264c0d..c38ed11f 100644 --- a/src/components/common/messaging/MessageBox.tsx +++ b/src/components/common/messaging/MessageBox.tsx @@ -55,11 +55,11 @@ export type UploadState = | { type: "none" } | { type: "attached"; files: File[] } | { - type: "uploading"; - files: File[]; - percent: number; - cancel: CancelTokenSource; - } + type: "uploading"; + files: File[]; + percent: number; + cancel: CancelTokenSource; + } | { type: "sending"; files: File[] } | { type: "failed"; files: File[]; error: string }; @@ -259,7 +259,7 @@ export default observer(({ channel }: Props) => { } console.log(channel) //|| channel.channel_type != "DirectMessage" if (channel.channel_type != "SavedMessages") - if (!channel.havePermission("SendMessage") && channel.channel_type == "TextChannel" || channel.recipient?.relationship == "Blocked" || channel.recipient?.relationship == "BlockedOther"){ + if (!channel.havePermission("SendMessage") && channel.channel_type == "TextChannel" || channel.recipient?.relationship == "Blocked" || channel.recipient?.relationship == "BlockedOther") { return ( @@ -299,9 +299,9 @@ export default observer(({ channel }: Props) => { const text = action === "quote" ? `${content - .split("\n") - .map((x) => `> ${x}`) - .join("\n")}\n\n` + .split("\n") + .map((x) => `> ${x}`) + .join("\n")}\n\n` : `${content} `; if (!state.draft.has(channel._id)) { @@ -355,8 +355,8 @@ export default observer(({ channel }: Props) => { toReplace == "" ? msg.content.toString() + newText : msg.content - .toString() - .replace(new RegExp(toReplace, flags), newText); + .toString() + .replace(new RegExp(toReplace, flags), newText); if (newContent != msg.content) { if (newContent.length == 0) { @@ -434,10 +434,10 @@ export default observer(({ channel }: Props) => { files, percent: Math.round( (i * 100 + (100 * e.loaded) / e.total) / - Math.min( - files.length, - CAN_UPLOAD_AT_ONCE, - ), + Math.min( + files.length, + CAN_UPLOAD_AT_ONCE, + ), ), cancel, }), @@ -630,42 +630,42 @@ export default observer(({ channel }: Props) => { {/* {channel.havePermission("UploadFiles") ? ( */} - - - setUploadState({ type: "none" }) - } - onChange={(files) => - setUploadState({ type: "attached", files }) - } - cancel={() => - uploadState.type === "uploading" && - uploadState.cancel.cancel("cancel") - } - append={(files) => { - if (files.length === 0) return; + + + setUploadState({ type: "none" }) + } + onChange={(files) => + setUploadState({ type: "attached", files }) + } + cancel={() => + uploadState.type === "uploading" && + uploadState.cancel.cancel("cancel") + } + append={(files) => { + if (files.length === 0) return; - if (uploadState.type === "none") { - setUploadState({ type: "attached", files }); - } else if (uploadState.type === "attached") { - setUploadState({ - type: "attached", - files: [...uploadState.files, ...files], - }); - } - }} - /> - + if (uploadState.type === "none") { + setUploadState({ type: "attached", files }); + } else if (uploadState.type === "attached") { + setUploadState({ + type: "attached", + files: [...uploadState.files, ...files], + }); + } + }} + /> + {/* ) : ( )} */} @@ -728,13 +728,13 @@ export default observer(({ channel }: Props) => { placeholder={ channel.channel_type === "DirectMessage" ? translate("app.main.channel.message_who", { - person: channel.recipient?.username, - }) + person: channel.recipient?.username, + }) : channel.channel_type === "SavedMessages" - ? translate("app.main.channel.message_saved") - : translate("app.main.channel.message_where", { - channel_name: channel.name ?? undefined, - }) + ? translate("app.main.channel.message_saved") + : translate("app.main.channel.message_where", { + channel_name: channel.name ?? undefined, + }) } disabled={ uploadState.type === "uploading" || diff --git a/src/components/common/messaging/PinMessageBox.tsx b/src/components/common/messaging/PinMessageBox.tsx new file mode 100644 index 00000000..4fa9ba28 --- /dev/null +++ b/src/components/common/messaging/PinMessageBox.tsx @@ -0,0 +1,162 @@ +import { + InfoCircle, + UserPlus, + UserMinus, + ArrowToRight, + ArrowToLeft, + UserX, + ShieldX, + EditAlt, + Edit, + MessageSquareEdit, + Key, +} from "@styled-icons/boxicons-solid"; +import { observer } from "mobx-react-lite"; +import { Message, Channel, API } from "revolt.js"; +import styled from "styled-components/macro"; +import { decodeTime } from "ulid"; + +import { useTriggerEvents } from "preact-context-menu"; +import { Text } from "preact-i18n"; + +import { Row } from "@revoltchat/ui"; + +import { TextReact } from "../../../lib/i18n"; + +import { useApplicationState } from "../../../mobx/State"; + +import { dayjs } from "../../../context/Locale"; + +import Markdown from "../../markdown/Markdown"; +import Tooltip from "../Tooltip"; +import UserShort from "../user/UserShort"; +import MessageBase, { MessageDetail, MessageInfo } from "./MessageBase"; +import { Pin } from "@styled-icons/boxicons-regular"; +import { useHistory } from "react-router-dom"; + +const SystemContent = styled.div` + gap: 4px; + display: flex; + padding: 2px 0; + flex-wrap: wrap; + align-items: center; + flex-direction: row; + font-size: 14px; + color: var(--secondary-foreground); + + span { + font-weight: 600; + color: var(--foreground); + } + + svg { + margin-inline-end: 4px; + } + + svg, + span { + cursor: pointer; + } + + span:hover { + text-decoration: underline; + } +`; + +interface Props { + attachContext?: boolean; + message: Message; + highlight?: boolean; + hideInfo?: boolean; + channel: Channel +} + +const iconDictionary = { + user_added: UserPlus, + user_remove: UserMinus, + user_joined: ArrowToRight, + user_left: ArrowToLeft, + user_kicked: UserX, + user_banned: ShieldX, + channel_renamed: EditAlt, + channel_description_changed: Edit, + channel_icon_changed: MessageSquareEdit, + channel_ownership_changed: Key, + text: InfoCircle, +}; + +export const PinMessageBox = observer( + ({ attachContext, message, channel, highlight, hideInfo }: Props) => { + const data: any = message.system + if (!data) return null; + const history = useHistory(); + + + let children = null; + let userName = message.client ? message.client.user?.username : "" + + + if (data.type as string == "message_pinned") { + children = children = ( +
{ + if (channel.channel_type === "TextChannel") { + history.push( + `/server/${channel.server_id}/channel/${channel._id}/${data.id}`, + ); + } else { + history.push(`/channel/${channel._id}/${data.id}`); + } + }} + > + +
+ ); + } + if (data.type as string == "message_unpinned") { + children = children = ( +
{ + if (channel.channel_type === "TextChannel") { + history.push( + `/server/${channel.server_id}/channel/${channel._id}/${data.id}`, + ); + } else { + history.push(`/channel/${channel._id}/${data.id}`); + } + }} + > + +
+ ); + } + + + + return ( + + + + {!hideInfo && ( + + + {/* */} + + )} + + + {children} + + ); + }, +); diff --git a/src/components/common/messaging/bars/PinnedMessage.tsx b/src/components/common/messaging/bars/PinnedMessage.tsx new file mode 100644 index 00000000..89891f9a --- /dev/null +++ b/src/components/common/messaging/bars/PinnedMessage.tsx @@ -0,0 +1,418 @@ +import { LeftArrow, LeftArrowAlt, Pin, UpArrowAlt } from "@styled-icons/boxicons-regular"; +import { observer } from "mobx-react-lite"; +import { useHistory } from "react-router-dom"; +import { Channel } from "revolt.js"; +import { decodeTime } from "ulid"; +import { isDesktop, isMobile, isTablet } from "react-device-detect"; + +import { Text } from "preact-i18n"; +import { useEffect, useState } from "preact/hooks"; + +import { internalSubscribe } from "../../../../lib/eventEmitter"; +import { getRenderer } from "../../../../lib/renderer/Singleton"; + +import { dayjs } from "../../../../context/Locale"; +import styled, { css } from "styled-components/macro"; + +import classNames from "classnames"; +import { isTouchscreenDevice } from "../../../../lib/isTouchscreenDevice"; +import { useClient } from "../../../../controllers/client/ClientController"; +import Message from "../Message"; +import { API, Message as MessageI, Nullable } from "revolt.js"; + +export const PinBar = styled.div<{ position: "top" | "bottom"; accent?: boolean }>` + z-index: 2; + position: relative; + + @keyframes bottomBounce { + 0% { + transform: translateY(33px); + } + 100% { + transform: translateY(0px); + } + } + + @keyframes topBounce { + 0% { + transform: translateY(-33px); + } + 100% { + transform: translateY(0px); + } + } + + ${(props) => + props.position === "top" && + css` + top: 0; + animation: topBounce 1s cubic-bezier(0.2, 0.9, 0.5, 1.16) + forwards; + `} + + ${(props) => + props.position === "bottom" && + css` + top: -28px; + animation: bottomBounce 340ms cubic-bezier(0.2, 0.9, 0.5, 1.16) + forwards; + + ${() => + isTouchscreenDevice && + css` + top: -90px; + `} + `} + + > div { + ${() => + isMobile ? + css` + width: 100%; + ` : isDesktop ? + css` + width: 40%;` + : + css` + width: 70%; + ` + } + right : 0px !important; + height: auto; + max-height: 600px; + min-height: 120px; + position: absolute; + display: block; + align-items: center; + cursor: pointer; + font-size: 12px; + font-weight: 600; + padding: 0 8px; + user-select: none; + justify-content: space-between; + transition: color ease-in-out 0.08s; + + white-space: nowrap; + overflow: scroll; + text-overflow: ellipsis; + + ${(props) => + props.accent + ? css` + color: var(--accent-contrast); + background-color: var(--hover)!important; + backdrop-filter: blur(20px); + ` + : css` + color: var(--secondary-foreground); + background-color: rgba( + var(--secondary-background-rgb), + max(var(--min-opacity), 0.9) + ); + backdrop-filter: blur(20px); + `} + + ${(props) => + props.position === "top" + ? css` + top: 48px; + border-radius: 0 0 var(--border-radius) + var(--border-radius); + ` + : css` + border-radius: var(--border-radius) var(--border-radius) 0 + 0; + `} + + ${() => + isTouchscreenDevice && + css` + top: 56px; + `} + + > div { + display: flex; + align-items: center; + gap: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &:hover { + color: var(--primary-text); + } + + &:active { + transform: translateY(1px); + } + + ${() => + isTouchscreenDevice && + css` + height: 34px; + padding: 0 12px; + `} + } + + @media only screen and (max-width: 800px) { + .right > span { + display: none; + } + } +`; + + + + + +export const PinIcon = styled.div<{ position: "top" | "bottom", accent?: boolean }>` + z-index: 2; + position: relative; + + @keyframes bottomBounce { + 0% { + transform: translateY(33px); + } + 100% { + transform: translateY(0px); + } + } + + @keyframes topBounce { + 0% { + transform: translateY(-33px); + } + 100% { + transform: translateY(0px); + } + } + ${(props) => + props.accent + ? css` + color: var(--accent-contrast); + background-color: var(--hover)!important; + backdrop-filter: blur(20px); + ` + : css` + color: var(--secondary-foreground); + background-color: rgba( + var(--secondary-background-rgb), + max(var(--min-opacity), 0.9) + ); + backdrop-filter: blur(20px); + `} + + ${(props) => + props.position === "top" && + css` + top: 5; + animation: topBounce 1s cubic-bezier(0.2, 0.9, 0.5, 1.16) + forwards; + `} + + + > div { + height: auto; + width: auto; + right : 5px !important; + position: absolute; + display: flex; + align-items: center; + cursor: pointer; + font-size: 12px; + font-weight: 600; + padding: 8px 8px; + user-select: none; + justify-content: space-between; + transition: color ease-in-out 0.08s; + + white-space: nowrap; + + ${(props) => + props.accent + ? css` + color: var(--accent-contrast); + background-color: var(--hover)!important; + backdrop-filter: blur(20px); + ` + : css` + color: var(--secondary-foreground); + background-color: rgba( + var(--secondary-background-rgb), + max(var(--min-opacity), 0.9) + ); + backdrop-filter: blur(20px); + `} + + ${(props) => + props.position === "top" + ? css` + top: 52px; + border-radius: 0 0 var(--border-radius) + var(--border-radius); + ` + : css` + border-radius: var(--border-radius) var(--border-radius) 0 + 0; + `} + + ${() => + isTouchscreenDevice && + css` + top: 56px; + `} + + + } + + @media only screen and (max-width: 800px) { + .right > span { + display: none; + } + } +`; + + + + +export default observer( + ({ channel }: { channel: Channel; }) => { + const [hidden, setHidden] = useState(true); + const unhide = () => setHidden(false); + const renderer = getRenderer(channel); + useEffect(() => { + // Subscribe to the update event for pinned messages + const unsubscribe = internalSubscribe( + "PinnedMessage", + "update", + (newMessage: unknown) => { + const message = newMessage as MessageI; + if (!renderer.pinned_messages.find((msg) => msg._id === message._id)) { + renderer.pinned_messages.push(message); + } + } + ); + + // Cleanup subscription on unmount + return () => unsubscribe(); + }, [renderer]); + + + const history = useHistory(); + if (renderer.state !== "RENDER") return null; + function truncateText(text: string, chars: number) { + if (text.length > chars) { + return text.slice(0, chars) + ".."; + } + return text; + } + const client = useClient() + + + let pinFound = false + return ( + <> + {channel.channel_type != "DirectMessage" && ( + +
unhide()}> + +
+
+ )} + {!hidden && +
+
setHidden(true)} + style={{ + backgroundColor: "var(--block)", + width: "100%", + position: "sticky", + top: "0px", + display: "flex", + zIndex: 2, + justifyContent: "space-between", + borderRadius: "5px", + padding: "8px 8px" + + }}> + + setHidden(true)} /> + + + +
+ + + +
+ { + + renderer.pinned_messages.slice().reverse().map((msg, i) => { + if (msg.is_pinned) { + let content = msg.content ? truncateText(msg.content, 220) : "" + pinFound = true + return ( + +
{ + // setHidden(true); + if (channel.channel_type === "TextChannel") { + history.push( + `/server/${channel.server_id}/channel/${channel._id}/${msg._id}`, + ); + } else { + history.push(`/channel/${channel._id}/${msg._id}`); + } + setHidden(true) + }} + style={{ display: 'flex', paddingTop: "5px" }} + > + +
+ ) + } + + }) + + + + } + + {!renderer.atTop &&
{ + // setHidden(true); + renderer.loadTop() + }} + + + style={{ display: 'flex', paddingTop: "5px", justifyContent: "center" }}> + + {/* */} +
} + +
+ + + + +
+
} + + ); + }, +); diff --git a/src/controllers/modals/components/CreateInvite.tsx b/src/controllers/modals/components/CreateInvite.tsx index d8a43273..b666143c 100644 --- a/src/controllers/modals/components/CreateInvite.tsx +++ b/src/controllers/modals/components/CreateInvite.tsx @@ -37,6 +37,7 @@ export default function CreateInvite({ }: ModalProps<"create_invite">) { const [processing, setProcessing] = useState(false); const [code, setCode] = useState("abcdef"); + const [url, setUrl] = useState("abcdef"); // Generate an invite code useEffect(() => { @@ -44,7 +45,10 @@ export default function CreateInvite({ target .createInvite() - .then(({ _id }) => setCode(_id)) + .then((res) => { + setUrl(res.url || "default_url"); + setCode(res._id || "default_code"); + }) .catch((err) => modalController.push({ type: "error", error: takeError(err) }), ) @@ -86,4 +90,3 @@ export default function CreateInvite({ /> ); } - diff --git a/src/lib/ContextMenus.tsx b/src/lib/ContextMenus.tsx index b51fd8cc..101b359d 100644 --- a/src/lib/ContextMenus.tsx +++ b/src/lib/ContextMenus.tsx @@ -58,6 +58,8 @@ type Action = | { action: "mark_as_read"; channel: Channel } | { action: "mark_server_as_read"; server: Server } | { action: "mark_unread"; message: Message } + | { action: "pin_message"; channel: any; message: any } + | { action: "unpin_message"; channel: any; message: any } | { action: "retry_message"; message: QueuedMessage } | { action: "cancel_message"; message: QueuedMessage } | { action: "mention"; user: string } @@ -87,32 +89,32 @@ type Action = | { action: "create_channel"; target: Server } | { action: "create_category"; target: Server } | { - action: "create_invite"; - target: Channel; - } + action: "create_invite"; + target: Channel; + } | { action: "leave_group"; target: Channel } | { - action: "delete_channel"; - target: Channel; - } + action: "delete_channel"; + target: Channel; + } | { action: "close_dm"; target: Channel } | { action: "leave_server"; target: Server } | { action: "delete_server"; target: Server } | { action: "edit_identity"; target: Member } | { - action: "open_notification_options"; - channel?: Channel; - server?: Server; - } + action: "open_notification_options"; + channel?: Channel; + server?: Server; + } | { action: "open_settings" } | { action: "open_channel_settings"; id: string } | { action: "open_server_settings"; id: string } | { action: "open_server_channel_settings"; server: string; id: string } | { - action: "set_notification_state"; - key: string; - state?: NotificationState; - } + action: "set_notification_state"; + key: string; + state?: NotificationState; + } | { action: "report"; target: User | Server | Message; messageId?: string }; // ! FIXME: I dare someone to re-write this @@ -202,8 +204,54 @@ export default function ContextMenus() { internalEmit("NewMessages", "mark", unread_id); data.message.channel?.ack(unread_id, true); } + case "pin_message": + { + + + const messages = getRenderer( + data.message.channel!, + ).messages; + const index = messages.findIndex( + (x) => x._id === data.message._id, + ); + + let message + + if (index > -1) { + message = messages[index]; + } + + if (message) { + internalEmit("PinnedMessage", "update", message); + } + internalEmit("MessageBox", "pin", message); + + // data.message.channel?.ack(pin_id, true); + } break; + + case "unpin_message": + { + + + const messages = getRenderer( + data.message.channel!, + ).messages; + const index = messages.findIndex( + (x) => x._id === data.message._id, + ); + let message + + if (index > -1) { + message = messages[index]; + } + + internalEmit("MessageBox", "unpin", message); + + // data.message.channel?.ack(pin_id, true); + } + break; case "retry_message": { const nonce = data.message.id; @@ -513,9 +561,8 @@ export default function ContextMenus() { "Open User in Admin Panel" ) : ( )} @@ -573,7 +620,7 @@ export default function ContextMenus() { const user = uid ? client.users.get(uid) : undefined; const serverChannel = targetChannel && - (targetChannel.channel_type === "TextChannel") + (targetChannel.channel_type === "TextChannel") ? targetChannel : undefined; @@ -585,8 +632,8 @@ export default function ContextMenus() { (server ? server.permission : serverChannel - ? serverChannel.server?.permission - : 0) || 0; + ? serverChannel.server?.permission + : 0) || 0; const userPermissions = (user ? user.permission : 0) || 0; if (unread) { @@ -810,6 +857,24 @@ export default function ContextMenus() { action: "mark_unread", message, }); + if (sendPermission) { + + + if (message.is_pinned && channel?.channel_type != "DirectMessage") { + generateAction({ + action: "unpin_message", + channel, + message + }); + } else { + generateAction({ + action: "pin_message", + channel, + message + }); + } + + } if ( typeof message.content === "string" && @@ -880,8 +945,8 @@ export default function ContextMenus() { type === "Image" ? "open_image" : type === "Video" - ? "open_video" - : "open_file", + ? "open_video" + : "open_file", ); generateAction( @@ -892,8 +957,8 @@ export default function ContextMenus() { type === "Image" ? "save_image" : type === "Video" - ? "save_video" - : "save_file", + ? "save_video" + : "save_file", ); generateAction( @@ -929,8 +994,8 @@ export default function ContextMenus() { type === "Image" ? "open_image" : type === "Video" - ? "open_video" - : "open_file", + ? "open_video" + : "open_file", ); generateAction( @@ -941,8 +1006,8 @@ export default function ContextMenus() { type === "Image" ? "save_image" : type === "Video" - ? "save_video" - : "save_file", + ? "save_video" + : "save_file", ); generateAction( @@ -1130,8 +1195,8 @@ export default function ContextMenus() { type: cid ? "channel" : message - ? "message" - : "user", + ? "message" + : "user", }, "admin", ); @@ -1158,8 +1223,8 @@ export default function ContextMenus() { cid ? "copy_cid" : message - ? "copy_mid" - : "copy_uid", + ? "copy_mid" + : "copy_uid", ); } } diff --git a/src/lib/eventEmitter.ts b/src/lib/eventEmitter.ts index c54460a9..85cebc7c 100644 --- a/src/lib/eventEmitter.ts +++ b/src/lib/eventEmitter.ts @@ -25,6 +25,8 @@ export function internalEmit(ns: string, event: string, ...args: unknown[]) { // - Intermediate/open_profile // - Intermediate/navigate // - MessageBox/append +// - MessageBox/pin +// - MessageBox/unpin // - TextArea/focus // - ReplyBar/add // - Modal/close diff --git a/src/lib/renderer/Singleton.ts b/src/lib/renderer/Singleton.ts index 90c4d20d..09aa1f49 100644 --- a/src/lib/renderer/Singleton.ts +++ b/src/lib/renderer/Singleton.ts @@ -15,6 +15,7 @@ export class ChannelRenderer { atTop: Nullable = null; atBottom: Nullable = null; messages: Message[] = []; + pinned_messages: Message[] = []; currentRenderer: RendererRoutines = SimpleRenderer; diff --git a/src/lib/renderer/simple/SimpleRenderer.ts b/src/lib/renderer/simple/SimpleRenderer.ts index 98db77b6..52d89ec0 100644 --- a/src/lib/renderer/simple/SimpleRenderer.ts +++ b/src/lib/renderer/simple/SimpleRenderer.ts @@ -9,12 +9,14 @@ export const SimpleRenderer: RendererRoutines = { if (nearby) renderer.channel .fetchMessagesWithUsers({ nearby, limit: 100 }) - .then(({ messages }) => { + .then(({ messages, pinned_messages }) => { messages.sort((a, b) => a._id.localeCompare(b._id)); runInAction(() => { renderer.state = "RENDER"; renderer.messages = messages; + renderer.pinned_messages = pinned_messages; + renderer.atTop = false; renderer.atBottom = false; @@ -27,12 +29,12 @@ export const SimpleRenderer: RendererRoutines = { else renderer.channel .fetchMessagesWithUsers({}) - .then(({ messages }) => { + .then(({ messages, pinned_messages }) => { messages.reverse(); - runInAction(() => { renderer.state = "RENDER"; renderer.messages = messages; + renderer.pinned_messages = pinned_messages; renderer.atTop = messages.length < 50; renderer.atBottom = true; diff --git a/src/pages/channels/Channel.tsx b/src/pages/channels/Channel.tsx index bc2d96b7..3a9d022e 100644 --- a/src/pages/channels/Channel.tsx +++ b/src/pages/channels/Channel.tsx @@ -26,6 +26,7 @@ import { PageHeader } from "../../components/ui/Header"; import { useClient } from "../../controllers/client/ClientController"; import ChannelHeader from "./ChannelHeader"; import { MessageArea } from "./messaging/MessageArea"; +import PinnedMessage from "../../components/common/messaging/bars/PinnedMessage"; const ChannelMain = styled.div.attrs({ "data-component": "channel" })` flex-grow: 1; @@ -98,9 +99,10 @@ export const Channel = observer( ({ id, server_id }: { id: string; server_id: string }) => { const client = useClient(); const state = useApplicationState(); - if (!client.channels.get(id)) { + + if (!client.channels.get(id)) { if (server_id) { - const server = client.servers.get(server_id); + const server = client.servers.get(server_id); if (server && server.channel_ids.length > 0) { let target_id = server.channel_ids[0]; const last_id = state.layout.getLastOpened(server_id); @@ -109,7 +111,7 @@ export const Channel = observer( target_id = last_id; } } - + return ( { + diff --git a/src/pages/channels/messaging/MessageArea.tsx b/src/pages/channels/messaging/MessageArea.tsx index d0ae8a5f..375d1a39 100644 --- a/src/pages/channels/messaging/MessageArea.tsx +++ b/src/pages/channels/messaging/MessageArea.tsx @@ -23,11 +23,12 @@ import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter"; import { getRenderer } from "../../../lib/renderer/Singleton"; import { ScrollState } from "../../../lib/renderer/types"; -import { useSession } from "../../../controllers/client/ClientController"; +import { useClient, useSession } from "../../../controllers/client/ClientController"; import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; import { modalController } from "../../../controllers/modals/ModalController"; import ConversationStart from "./ConversationStart"; import MessageRenderer from "./MessageRenderer"; +import { Message } from "revolt.js/esm"; const Area = styled.div.attrs({ "data-scroll-offset": "with-padding" })` height: 100%; @@ -115,8 +116,8 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { 101, ref.current ? ref.current.scrollTop + - (ref.current.scrollHeight - - scrollState.current.previousHeight) + (ref.current.scrollHeight - + scrollState.current.previousHeight) : 101, ), { @@ -148,20 +149,48 @@ export const MessageArea = observer(({ last_id, channel }: Props) => { const atBottom = (offset = 0) => ref.current ? Math.floor(ref.current?.scrollHeight - ref.current?.scrollTop) - - offset <= - ref.current?.clientHeight + offset <= + ref.current?.clientHeight : true; const atTop = (offset = 0) => ref.current ? ref.current.scrollTop <= offset : false; + const client = useClient() + function pin(message: Message) { + client.api.post(`/channels/${message.channel_id}/messages/${message._id}/pin` as any) + message.is_pinned = true + } + function unpin(message: Message) { + client.api.delete(`/channels/${message.channel_id}/messages/${message._id}/pin` as any) + message.is_pinned = false + } // ? Handle global jump to bottom, e.g. when editing last message in chat. useEffect(() => { + return internalSubscribe("MessageArea", "jump_to_bottom", () => setScrollState({ type: "ScrollToBottom" }), ); }, [setScrollState]); + useEffect(() => { + + + return internalSubscribe( + "MessageBox", + "pin", + pin as (...args: unknown[]) => void, + ); + }, []); + useEffect(() => { + + + return internalSubscribe( + "MessageBox", + "unpin", + unpin as (...args: unknown[]) => void, + ); + }, []); // ? Handle events from renderer. useLayoutEffect( () => setScrollState(renderer.scrollState), diff --git a/src/pages/channels/messaging/MessageRenderer.tsx b/src/pages/channels/messaging/MessageRenderer.tsx index 950c56d5..c9ad66a3 100644 --- a/src/pages/channels/messaging/MessageRenderer.tsx +++ b/src/pages/channels/messaging/MessageRenderer.tsx @@ -23,6 +23,7 @@ import { useClient } from "../../../controllers/client/ClientController"; import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline"; import ConversationStart from "./ConversationStart"; import MessageEditor from "./MessageEditor"; +import { PinMessageBox } from "../../../components/common/messaging/PinMessageBox"; interface Props { last_id?: string; @@ -150,8 +151,9 @@ export default observer(({ last_id, renderer, highlight }: Props) => { ); blocked = 0; } + let lastPinned = null - for (const message of renderer.messages) { + for (const [i, message] of renderer.messages.entries()) { if (previous) { compare( message._id, @@ -162,8 +164,21 @@ export default observer(({ last_id, renderer, highlight }: Props) => { previous.masquerade, ); } + // console.log(renderer.messages[i].content, 7979) - if (message.author_id === "00000000000000000000000000") { + + if (message.system?.type as any == "message_pinned" || message.system?.type as any == "message_unpinned") { + render.push( + + , + ); + } else if (message.author_id === "00000000000000000000000000") { render.push( { attachContext highlight={highlight === message._id} />, - ); + ) } else if (message.author?.relationship === "Blocked") { blocked++; } else { @@ -204,6 +219,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => { const nonces = renderer.messages.map((x) => x.nonce); if (renderer.atBottom) { for (const msg of queue.get(renderer.channel._id)) { + if (nonces.includes(msg.id)) continue; if (previous) { @@ -222,6 +238,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => { } as MessageI; } + render.push(