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" && (
+
+
+
+ )}
+ {!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(