Merge pull request #35 from archem-team/feat/pin_message

Feat/pin message
pull/1154/head
teamabron 2025-02-04 08:30:01 +03:30 committed by GitHub
commit 29f8668a32
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 816 additions and 109 deletions

6
.gitmodules vendored
View File

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

View File

@ -1,4 +1,7 @@
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"[typescriptreact]": {
"editor.defaultFormatter": "vscode.typescript-language-features"
}
}

2
external/lang vendored

@ -1 +1 @@
Subproject commit 3195d642cd766cb62d34eb2a57ce3a09e775e91f
Subproject commit 8ecb9a34b1b459b5280a6351a4044dfa44b68019

2
external/revolt.js vendored

@ -1 +1 @@
Subproject commit cd9e84a337c72709b82bb4eca794ec7474a0ee7e
Subproject commit 007702579cd6e611fce79498461e89951387108d

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 };
@ -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 (
<Base>
@ -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) => {
</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 />
)} */}
@ -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" ||

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,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" && (
<PinIcon position="top" accent>
<div onClick={() => unhide()}>
<Pin size={24} />
</div>
</PinIcon>
)}
{!hidden && <PinBar accent position="top" >
<div style={{ height: 'auto' }}>
<div
onClick={() => setHidden(true)}
style={{
backgroundColor: "var(--block)",
width: "100%",
position: "sticky",
top: "0px",
display: "flex",
zIndex: 2,
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.pinned_messages.slice().reverse().map((msg, i) => {
if (msg.is_pinned) {
let content = msg.content ? truncateText(msg.content, 220) : ""
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}`);
}
setHidden(true)
}}
style={{ display: 'flex', paddingTop: "5px" }}
>
<Message
message={msg}
key={msg._id}
head={true}
content={
undefined
}
/>
</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

@ -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({
/>
);
}

View File

@ -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"
) : (
<Text
id={`app.context_menu.${
locale ?? action.action
}`}
id={`app.context_menu.${locale ?? action.action
}`}
/>
)}
</span>
@ -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",
);
}
}

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

@ -15,6 +15,7 @@ export class ChannelRenderer {
atTop: Nullable<boolean> = null;
atBottom: Nullable<boolean> = null;
messages: Message[] = [];
pinned_messages: Message[] = [];
currentRenderer: RendererRoutines = SimpleRenderer;

View File

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

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;
@ -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 (
<Redirect
to={`/server/${server_id}/channel/${target_id}`}
@ -187,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={