Add pin message

pull/1154/head
TeamAbron 2025-01-18 15:06:06 +03:30
parent c1771da8cc
commit e3f5a01f2e
10 changed files with 896 additions and 163 deletions

View File

@ -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 };
@ -257,25 +257,27 @@ export default observer(({ channel }: Props) => {
</Base>
);
}
console.log(channel)
if (!channel.havePermission("SendMessage") && channel.recipient?.relationship == "Blocked" || channel.recipient?.relationship == "BlockedOther"){
return (
<Base>
<Blocked>
<Action>
<PermissionTooltip
permission="SendMessages"
placement="top">
<ShieldX size={22} />
</PermissionTooltip>
</Action>
<div className="text">
<Text id="app.main.channel.misc.no_sending" />
</div>
</Blocked>
</Base>
);
}
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") {
return (
<Base>
<Blocked>
<Action>
<PermissionTooltip
permission="SendMessages"
placement="top">
<ShieldX size={22} />
</PermissionTooltip>
</Action>
<div className="text">
<Text id="app.main.channel.misc.no_sending" />
</div>
</Blocked>
</Base>
);
}
// Push message content to draft.
const setMessage = useCallback(
(content?: string) => {
@ -297,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)) {
@ -353,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) {
@ -432,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,
}),
@ -628,42 +630,42 @@ export default observer(({ channel }: Props) => {
</FloatingLayer>
<Base>
{/* {channel.havePermission("UploadFiles") ? ( */}
<FileAction>
<FileUploader
size={24}
behaviour="multi"
style="attachment"
fileType="attachments"
maxFileSize={20_000_000}
attached={uploadState.type !== "none"}
uploading={
uploadState.type === "uploading" ||
uploadState.type === "sending"
}
remove={async () =>
setUploadState({ type: "none" })
}
onChange={(files) =>
setUploadState({ type: "attached", files })
}
cancel={() =>
uploadState.type === "uploading" &&
uploadState.cancel.cancel("cancel")
}
append={(files) => {
if (files.length === 0) return;
<FileAction>
<FileUploader
size={24}
behaviour="multi"
style="attachment"
fileType="attachments"
maxFileSize={20_000_000}
attached={uploadState.type !== "none"}
uploading={
uploadState.type === "uploading" ||
uploadState.type === "sending"
}
remove={async () =>
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],
});
}
}}
/>
</FileAction>
if (uploadState.type === "none") {
setUploadState({ type: "attached", files });
} else if (uploadState.type === "attached") {
setUploadState({
type: "attached",
files: [...uploadState.files, ...files],
});
}
}}
/>
</FileAction>
{/* ) : (
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
)} */}
@ -726,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" ||

View File

@ -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 = (
<div
onClick={() => {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${channel._id}/${data.id}`,
);
} else {
history.push(`/channel/${channel._id}/${data.id}`);
}
}}
>
<TextReact
id={`app.main.channel.system.message_pinned`}
fields={{
user: userName,
}}
/>
</div>
);
}
if (data.type as string == "message_unpinned") {
children = children = (
<div
onClick={() => {
if (channel.channel_type === "TextChannel") {
history.push(
`/server/${channel.server_id}/channel/${channel._id}/${data.id}`,
);
} else {
history.push(`/channel/${channel._id}/${data.id}`);
}
}}
>
<TextReact
id={`app.main.channel.system.message_unpinned`}
fields={{
user: userName,
}}
/>
</div>
);
}
return (
<MessageBase highlight={highlight}>
{!hideInfo && (
<MessageInfo click={false}>
<MessageDetail message={message} position="left" />
{/* <SystemMessageIcon className="systemIcon" /> */}
</MessageInfo>
)}
<SystemContent style={{ height: 20, fontSize: 12, display: "block", width: "100%", textAlign: "center", cursor: "pointer" }}>{children}</SystemContent>
</MessageBase>
);
},
);

View File

@ -0,0 +1,423 @@
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 { 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 "revolt.js/esm";
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 {
min-height: 120px;
max-height: 200px;
height: auto;
width: 40%;
right : 0px !important;
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);
// useEffect(() => setHidden(false), [last_id]);
// useEffect(() => internalSubscribe("NewMessages", "hide", hide), []);
// useEffect(() => {
// const onKeyDown = (e: KeyboardEvent) =>
// e.key === "Escape" && hide();
// document.addEventListener("keydown", onKeyDown);
// return () => document.removeEventListener("keydown", onKeyDown);
// }, []);
// const extendedMessage = new MessageExtendedClass(client);
// useEffect(() => {
// if (last_id) {
// try {
// setTimeAgo(dayjs(decodeTime(last_id)).fromNow());
// } catch (err) { }
// }
// }, [last_id]);
const renderer = getRenderer(channel);
const history = useHistory();
if (renderer.state !== "RENDER") return null;
// if (!last_id) return null;
// if (hidden) return null;
// renderer.messages.slice().reverse().map((res, i) => {
// console.log(res, 8989)
// })
function truncateText(text: string, chars: number) {
if (text.length > chars) {
return text.slice(0, chars) + "..";
}
return text;
}
let pinFound = false
return (
<>
<PinIcon position="top" accent>
<div
onClick={() => unhide()}
>
<Pin size={24} />
</div>
</PinIcon>
{!hidden && <PinBar accent position="top" >
<div>
<div
onClick={() => setHidden(true)}
style={{
backgroundColor: "var(--block)",
width: "100%",
position: "sticky",
top: "0px",
display: "flex",
justifyContent: "space-between",
borderRadius: "5px",
padding: "8px 8px"
}}>
<LeftArrowAlt size={20} onClick={() => setHidden(true)} />
<Text
id="app.main.channel.misc.pinned_message_title"
/>
<Pin size={20} />
</div>
<div style={{ display: 'grid', flexDirection: "column" }} >
{
renderer.messages.slice().reverse().map((msg, i) => {
if (msg.is_pinned) {
// console.log(msg, 8989)
let content = msg.content ? truncateText(msg.content, 20) : ""
pinFound = true
return (
<div
onClick={() => {
// 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}`);
}
}}
style={{ display: 'flex', paddingTop: "5px" }}>
<>. {" "}</>
<Text
id="app.main.channel.misc.pinned_message"
fields={{
message_summery: content,
}}
/>
</div>
)
}
})
}
{!renderer.atTop && <div
onClick={() => {
// setHidden(true);
renderer.loadTop()
}}
style={{ display: 'flex', paddingTop: "5px", justifyContent: "center" }}>
<Text
id="app.main.channel.misc.pinned_load_more"
/>
</div>}
</div>
</div>
</PinBar>}
</>
);
},
);

View File

@ -58,6 +58,7 @@ 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: "retry_message"; message: QueuedMessage }
| { action: "cancel_message"; message: QueuedMessage }
| { action: "mention"; user: string }
@ -87,32 +88,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 +203,50 @@ 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 > 0) {
message = messages[index];
}
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 > 0) {
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 +556,8 @@ export default function ContextMenus() {
"Open User in Admin Panel"
) : (
<Text
id={`app.context_menu.${
locale ?? action.action
}`}
id={`app.context_menu.${locale ?? action.action
}`}
/>
)}
</span>
@ -573,7 +615,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 +627,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 +852,24 @@ export default function ContextMenus() {
action: "mark_unread",
message,
});
if (sendPermission) {
if (message.is_pinned) {
generateAction({
action: "unpin_message",
channel,
message
});
} else {
generateAction({
action: "pin_message",
channel,
message
});
}
}
if (
typeof message.content === "string" &&
@ -880,8 +940,8 @@ export default function ContextMenus() {
type === "Image"
? "open_image"
: type === "Video"
? "open_video"
: "open_file",
? "open_video"
: "open_file",
);
generateAction(
@ -892,8 +952,8 @@ export default function ContextMenus() {
type === "Image"
? "save_image"
: type === "Video"
? "save_video"
: "save_file",
? "save_video"
: "save_file",
);
generateAction(
@ -929,8 +989,8 @@ export default function ContextMenus() {
type === "Image"
? "open_image"
: type === "Video"
? "open_video"
: "open_file",
? "open_video"
: "open_file",
);
generateAction(
@ -941,8 +1001,8 @@ export default function ContextMenus() {
type === "Image"
? "save_image"
: type === "Video"
? "save_video"
: "save_file",
? "save_video"
: "save_file",
);
generateAction(
@ -1130,8 +1190,8 @@ export default function ContextMenus() {
type: cid
? "channel"
: message
? "message"
: "user",
? "message"
: "user",
},
"admin",
);
@ -1158,8 +1218,8 @@ export default function ContextMenus() {
cid
? "copy_cid"
: message
? "copy_mid"
: "copy_uid",
? "copy_mid"
: "copy_uid",
);
}
}

View File

@ -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

View File

@ -28,6 +28,7 @@ export const SimpleRenderer: RendererRoutines = {
renderer.channel
.fetchMessagesWithUsers({})
.then(({ messages }) => {
console.log(messages, 9090);
messages.reverse();
runInAction(() => {

View File

@ -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;
@ -99,7 +100,7 @@ export const Channel = observer(
const client = useClient();
const state = useApplicationState();
if (!client.channels.exists(id) && client.servers.get(server_id)) {
if (!client.channels.exists(id) && server_id) {
if (server_id) {
const server = client.servers.get(server_id);
if (server && server.channel_ids.length > 0) {
@ -110,7 +111,7 @@ export const Channel = observer(
target_id = last_id;
}
}
return (
<Redirect
to={`/server/${server_id}/channel/${target_id}`}
@ -188,6 +189,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
<ErrorBoundary section="renderer">
<ChannelContent>
<NewMessages channel={channel} last_id={lastId} />
<PinnedMessage channel={channel} />
<MessageArea channel={channel} last_id={lastId} />
<TypingIndicator channel={channel} />
<JumpToBottom channel={channel} />

View File

@ -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),

View File

@ -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(
<PinMessageBox
key={message._id}
message={message}
attachContext
channel={renderer.channel}
highlight={highlight === message._id}
/>
,
);
} else if (message.author_id === "00000000000000000000000000") {
render.push(
<SystemMessage
key={message._id}
@ -171,7 +186,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
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(
<Message
message={

129
yarn.lock
View File

@ -1841,6 +1841,13 @@ __metadata:
languageName: node
linkType: hard
"@fastify/busboy@npm:^2.0.0":
version: 2.1.1
resolution: "@fastify/busboy@npm:2.1.1"
checksum: 42c32ef75e906c9a4809c1e1930a5ca6d4ddc8d138e1a8c8ba5ea07f997db32210617d23b2e4a85fe376316a41a1a0439fc6ff2dedf5126d96f45a9d80754fb2
languageName: node
linkType: hard
"@floating-ui/core@npm:^1.0.0":
version: 1.0.0
resolution: "@floating-ui/core@npm:1.0.0"
@ -5028,23 +5035,13 @@ __metadata:
languageName: node
linkType: hard
"follow-redirects@npm:^1.14.0":
version: 1.14.4
resolution: "follow-redirects@npm:1.14.4"
"follow-redirects@npm:^1.14.0, follow-redirects@npm:^1.14.8":
version: 1.15.9
resolution: "follow-redirects@npm:1.15.9"
peerDependenciesMeta:
debug:
optional: true
checksum: d4ce74cf5c6f363168b97e706b914eb9ffb6bf4d4c6d8f8330b93088d9b90e566611ddbcf0e42c8ed5fd17598dfeda1d19230d3e9d6d6c6b4d1c10ec3a0b99be
languageName: node
linkType: hard
"follow-redirects@npm:^1.14.8":
version: 1.14.9
resolution: "follow-redirects@npm:1.14.9"
peerDependenciesMeta:
debug:
optional: true
checksum: f5982e0eb481818642492d3ca35a86989c98af1128b8e1a62911a3410621bc15d2b079e8170b35b19d3bdee770b73ed431a257ed86195af773771145baa57845
checksum: 859e2bacc7a54506f2bf9aacb10d165df78c8c1b0ceb8023f966621b233717dab56e8d08baadc3ad3b9db58af290413d585c999694b7c146aaf2616340c3d2a6
languageName: node
linkType: hard
@ -7056,7 +7053,7 @@ __metadata:
languageName: node
linkType: hard
"mobx-react-lite@npm:3.4.0, mobx-react-lite@npm:^3.4.0":
"mobx-react-lite@npm:3.4.0":
version: 3.4.0
resolution: "mobx-react-lite@npm:3.4.0"
peerDependencies:
@ -7071,10 +7068,25 @@ __metadata:
languageName: node
linkType: hard
"mobx-react-lite@npm:^3.4.0":
version: 3.4.3
resolution: "mobx-react-lite@npm:3.4.3"
peerDependencies:
mobx: ^6.1.0
react: ^16.8.0 || ^17 || ^18
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 60a2580eb9a0b9988fc76959d7299f018733dc33cacaa73c45500953006d4d45e738d9ae39ccfc767ac19a75656dbc028d833282c848fbc67d8d18a2bcb5c262
languageName: node
linkType: hard
"mobx@npm:^6.3.2":
version: 6.3.2
resolution: "mobx@npm:6.3.2"
checksum: da853901ddd9bc5347f778b1aebfcbf5215d0cb0909fa17a9580dec2915b034b385e8cea6f2a85bd917078a29fcbb129c28be10e61db06c59bd3c94ace15f4a9
version: 6.13.5
resolution: "mobx@npm:6.13.5"
checksum: 2a253e505900169326873b573660dab58ce8284a435c75d77775a665af9eed623c1976a5eab3cfb53561cac4713f70194f49a2b8e4111419a2e4291f51e5485a
languageName: node
linkType: hard
@ -7279,18 +7291,18 @@ __metadata:
linkType: hard
"openapi-typescript@npm:^5.2.0":
version: 5.2.0
resolution: "openapi-typescript@npm:5.2.0"
version: 5.4.2
resolution: "openapi-typescript@npm:5.4.2"
dependencies:
js-yaml: ^4.1.0
mime: ^3.0.0
prettier: ^2.5.1
prettier: ^2.6.2
tiny-glob: ^0.2.9
undici: ^4.14.1
yargs-parser: ^21.0.0
undici: ^5.4.0
yargs-parser: ^21.0.1
bin:
openapi-typescript: bin/cli.js
checksum: 193b0d910d1c067dcbb0f079bf2ae29a3bd392cbae82265760e6e96a70221a3d8d7c287c77af845c2b4bfd4c04251f800c838c63a76564e9f1427b47e62fb598
checksum: 7777dfc99dff8971cdfa9c9d965ee6f464fd58ec81ee1add1450c354d7ca09fa48d5aacef27624a203e627cda375fc039516f9fbb2afdf6911b631329fc814a5
languageName: node
linkType: hard
@ -7508,12 +7520,12 @@ __metadata:
languageName: node
linkType: hard
"prettier@npm:^2.5.1":
version: 2.6.2
resolution: "prettier@npm:2.6.2"
"prettier@npm:^2.6.2":
version: 2.8.8
resolution: "prettier@npm:2.8.8"
bin:
prettier: bin-prettier.js
checksum: 48d08dde8e9fb1f5bccdd205baa7f192e9fc8bc98f86e1b97d919de804e28c806b0e6cc685e4a88211aa7987fa9668f30baae19580d87ced3ed0f2ec6572106f
checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8
languageName: node
linkType: hard
@ -7637,7 +7649,7 @@ __metadata:
languageName: node
linkType: hard
"react-device-detect@npm:2.2.2, react-device-detect@npm:^2.2.2":
"react-device-detect@npm:2.2.2":
version: 2.2.2
resolution: "react-device-detect@npm:2.2.2"
dependencies:
@ -7649,6 +7661,18 @@ __metadata:
languageName: node
linkType: hard
"react-device-detect@npm:^2.2.2":
version: 2.2.3
resolution: "react-device-detect@npm:2.2.3"
dependencies:
ua-parser-js: ^1.0.33
peerDependencies:
react: ">= 0.14.0"
react-dom: ">= 0.14.0"
checksum: 42d9b3182b9d2495bf0d7914c9f370da51d8bdb853a3eba2acaf433894ae760386a075ba103185be825b33d42f50d85ef462087f261656d433f4c74dab23861f
languageName: node
linkType: hard
"react-fast-compare@npm:^3.1.1":
version: 3.2.0
resolution: "react-fast-compare@npm:3.2.0"
@ -9068,12 +9092,12 @@ __metadata:
linkType: hard
"typescript@npm:^4.6.2":
version: 4.6.4
resolution: "typescript@npm:4.6.4"
version: 4.9.5
resolution: "typescript@npm:4.9.5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: e7bfcc39cd4571a63a54e5ea21f16b8445268b9900bf55aee0e02ad981be576acc140eba24f1af5e3c1457767c96cea6d12861768fb386cf3ffb34013718631a
checksum: ee000bc26848147ad423b581bd250075662a354d84f0e06eb76d3b892328d8d4440b7487b5a83e851b12b255f55d71835b008a66cbf8f255a11e4400159237db
languageName: node
linkType: hard
@ -9088,12 +9112,12 @@ __metadata:
linkType: hard
"typescript@patch:typescript@^4.6.2#~builtin<compat/typescript>":
version: 4.6.4
resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin<compat/typescript>::version=4.6.4&hash=bda367"
version: 4.9.5
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=bda367"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 1cb434fbc637d347be90e3a0c6cd05e33c38f941713c8786d3031faf1842c2c148ba91d2fac01e7276b0ae3249b8633f1660e32686cc7a8c6a8fd5361dc52c66
checksum: 2eee5c37cad4390385db5db5a8e81470e42e8f1401b0358d7390095d6f681b410f2c4a0c496c6ff9ebd775423c7785cdace7bcdad76c7bee283df3d9718c0f20
languageName: node
linkType: hard
@ -9104,6 +9128,15 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:^1.0.33":
version: 1.0.40
resolution: "ua-parser-js@npm:1.0.40"
bin:
ua-parser-js: script/cli.js
checksum: ae555a33dc9395dd877e295d6adbf5634e047aad7c3358328830218f3ca3a6233e35848cd355465a7612f269860e8029984389282940c7a27c9af4dfcdbba8c3
languageName: node
linkType: hard
"ulid@npm:^2.3.0":
version: 2.3.0
resolution: "ulid@npm:2.3.0"
@ -9125,10 +9158,12 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^4.14.1":
version: 4.16.0
resolution: "undici@npm:4.16.0"
checksum: 5e88c2b3381085e25ed1d1a308610ac7ee985f478ac705af7a8e03213536e10f73ef8dd8d85e6ed38948d1883fa0ae935e04357c317b0f5d3d3c0211d0c8c393
"undici@npm:^5.4.0":
version: 5.28.4
resolution: "undici@npm:5.28.4"
dependencies:
"@fastify/busboy": ^2.0.0
checksum: a8193132d84540e4dc1895ecc8dbaa176e8a49d26084d6fbe48a292e28397cd19ec5d13bc13e604484e76f94f6e334b2bdc740d5f06a6e50c44072818d0c19f9
languageName: node
linkType: hard
@ -9794,17 +9829,17 @@ __metadata:
linkType: hard
"ws@npm:^8.2.2":
version: 8.2.2
resolution: "ws@npm:8.2.2"
version: 8.18.0
resolution: "ws@npm:8.18.0"
peerDependencies:
bufferutil: ^4.0.1
utf-8-validate: ^5.0.2
utf-8-validate: ">=5.0.2"
peerDependenciesMeta:
bufferutil:
optional: true
utf-8-validate:
optional: true
checksum: 25e764c631141bdca45badc86e69437b8791e57e461f9a16c0f7cd779baf70c3fbba07ecdd9e0d34fea1155ddcf62ef165cd7f81b68ed545bc7d455c15a85fb0
checksum: 91d4d35bc99ff6df483bdf029b9ea4bfd7af1f16fc91231a96777a63d263e1eabf486e13a2353970efc534f9faa43bdbf9ee76525af22f4752cbc5ebda333975
languageName: node
linkType: hard
@ -9822,10 +9857,10 @@ __metadata:
languageName: node
linkType: hard
"yargs-parser@npm:^21.0.0":
version: 21.0.1
resolution: "yargs-parser@npm:21.0.1"
checksum: c3ea2ed12cad0377ce3096b3f138df8267edf7b1aa7d710cd502fe16af417bafe4443dd71b28158c22fcd1be5dfd0e86319597e47badf42ff83815485887323a
"yargs-parser@npm:^21.0.1":
version: 21.1.1
resolution: "yargs-parser@npm:21.1.1"
checksum: ed2d96a616a9e3e1cc7d204c62ecc61f7aaab633dcbfab2c6df50f7f87b393993fe6640d017759fe112d0cb1e0119f2b4150a87305cc873fd90831c6a58ccf1c
languageName: node
linkType: hard