mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-11 02:55:28 +00:00
Merge pull request #35 from archem-team/feat/pin_message
Feat/pin message
This commit is contained in:
6
.gitmodules
vendored
6
.gitmodules
vendored
@@ -1,10 +1,12 @@
|
|||||||
[submodule "external/lang"]
|
[submodule "external/lang"]
|
||||||
path = external/lang
|
path = external/lang
|
||||||
url = https://github.com/revoltchat/translations
|
url = https://github.com/archem-team/translations
|
||||||
|
branch = revite-backports
|
||||||
[submodule "external/components"]
|
[submodule "external/components"]
|
||||||
path = external/components
|
path = external/components
|
||||||
url = https://github.com/archem-team/components
|
url = https://github.com/archem-team/components
|
||||||
branch = bug/deleted_account_notif
|
branch = bug/deleted_account_notif
|
||||||
[submodule "external/revolt.js"]
|
[submodule "external/revolt.js"]
|
||||||
path = 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
|
||||||
|
|||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
external/lang
vendored
2
external/lang
vendored
Submodule external/lang updated: 3195d642cd...8ecb9a34b1
2
external/revolt.js
vendored
2
external/revolt.js
vendored
Submodule external/revolt.js updated: cd9e84a337...007702579c
162
src/components/common/messaging/PinMessageBox.tsx
Normal file
162
src/components/common/messaging/PinMessageBox.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
418
src/components/common/messaging/bars/PinnedMessage.tsx
Normal file
418
src/components/common/messaging/bars/PinnedMessage.tsx
Normal 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>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -37,6 +37,7 @@ export default function CreateInvite({
|
|||||||
}: ModalProps<"create_invite">) {
|
}: ModalProps<"create_invite">) {
|
||||||
const [processing, setProcessing] = useState(false);
|
const [processing, setProcessing] = useState(false);
|
||||||
const [code, setCode] = useState("abcdef");
|
const [code, setCode] = useState("abcdef");
|
||||||
|
const [url, setUrl] = useState("abcdef");
|
||||||
|
|
||||||
// Generate an invite code
|
// Generate an invite code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,7 +45,10 @@ export default function CreateInvite({
|
|||||||
|
|
||||||
target
|
target
|
||||||
.createInvite()
|
.createInvite()
|
||||||
.then(({ _id }) => setCode(_id))
|
.then((res) => {
|
||||||
|
setUrl(res.url || "default_url");
|
||||||
|
setCode(res._id || "default_code");
|
||||||
|
})
|
||||||
.catch((err) =>
|
.catch((err) =>
|
||||||
modalController.push({ type: "error", error: takeError(err) }),
|
modalController.push({ type: "error", error: takeError(err) }),
|
||||||
)
|
)
|
||||||
@@ -86,4 +90,3 @@ export default function CreateInvite({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ type Action =
|
|||||||
| { action: "mark_as_read"; channel: Channel }
|
| { action: "mark_as_read"; channel: Channel }
|
||||||
| { action: "mark_server_as_read"; server: Server }
|
| { action: "mark_server_as_read"; server: Server }
|
||||||
| { action: "mark_unread"; message: Message }
|
| { 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: "retry_message"; message: QueuedMessage }
|
||||||
| { action: "cancel_message"; message: QueuedMessage }
|
| { action: "cancel_message"; message: QueuedMessage }
|
||||||
| { action: "mention"; user: string }
|
| { action: "mention"; user: string }
|
||||||
@@ -202,8 +204,54 @@ export default function ContextMenus() {
|
|||||||
internalEmit("NewMessages", "mark", unread_id);
|
internalEmit("NewMessages", "mark", unread_id);
|
||||||
data.message.channel?.ack(unread_id, true);
|
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;
|
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":
|
case "retry_message":
|
||||||
{
|
{
|
||||||
const nonce = data.message.id;
|
const nonce = data.message.id;
|
||||||
@@ -513,8 +561,7 @@ export default function ContextMenus() {
|
|||||||
"Open User in Admin Panel"
|
"Open User in Admin Panel"
|
||||||
) : (
|
) : (
|
||||||
<Text
|
<Text
|
||||||
id={`app.context_menu.${
|
id={`app.context_menu.${locale ?? action.action
|
||||||
locale ?? action.action
|
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -810,6 +857,24 @@ export default function ContextMenus() {
|
|||||||
action: "mark_unread",
|
action: "mark_unread",
|
||||||
message,
|
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 (
|
if (
|
||||||
typeof message.content === "string" &&
|
typeof message.content === "string" &&
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ export function internalEmit(ns: string, event: string, ...args: unknown[]) {
|
|||||||
// - Intermediate/open_profile
|
// - Intermediate/open_profile
|
||||||
// - Intermediate/navigate
|
// - Intermediate/navigate
|
||||||
// - MessageBox/append
|
// - MessageBox/append
|
||||||
|
// - MessageBox/pin
|
||||||
|
// - MessageBox/unpin
|
||||||
// - TextArea/focus
|
// - TextArea/focus
|
||||||
// - ReplyBar/add
|
// - ReplyBar/add
|
||||||
// - Modal/close
|
// - Modal/close
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export class ChannelRenderer {
|
|||||||
atTop: Nullable<boolean> = null;
|
atTop: Nullable<boolean> = null;
|
||||||
atBottom: Nullable<boolean> = null;
|
atBottom: Nullable<boolean> = null;
|
||||||
messages: Message[] = [];
|
messages: Message[] = [];
|
||||||
|
pinned_messages: Message[] = [];
|
||||||
|
|
||||||
currentRenderer: RendererRoutines = SimpleRenderer;
|
currentRenderer: RendererRoutines = SimpleRenderer;
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ export const SimpleRenderer: RendererRoutines = {
|
|||||||
if (nearby)
|
if (nearby)
|
||||||
renderer.channel
|
renderer.channel
|
||||||
.fetchMessagesWithUsers({ nearby, limit: 100 })
|
.fetchMessagesWithUsers({ nearby, limit: 100 })
|
||||||
.then(({ messages }) => {
|
.then(({ messages, pinned_messages }) => {
|
||||||
messages.sort((a, b) => a._id.localeCompare(b._id));
|
messages.sort((a, b) => a._id.localeCompare(b._id));
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
renderer.state = "RENDER";
|
renderer.state = "RENDER";
|
||||||
renderer.messages = messages;
|
renderer.messages = messages;
|
||||||
|
renderer.pinned_messages = pinned_messages;
|
||||||
|
|
||||||
renderer.atTop = false;
|
renderer.atTop = false;
|
||||||
renderer.atBottom = false;
|
renderer.atBottom = false;
|
||||||
|
|
||||||
@@ -27,12 +29,12 @@ export const SimpleRenderer: RendererRoutines = {
|
|||||||
else
|
else
|
||||||
renderer.channel
|
renderer.channel
|
||||||
.fetchMessagesWithUsers({})
|
.fetchMessagesWithUsers({})
|
||||||
.then(({ messages }) => {
|
.then(({ messages, pinned_messages }) => {
|
||||||
messages.reverse();
|
messages.reverse();
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
renderer.state = "RENDER";
|
renderer.state = "RENDER";
|
||||||
renderer.messages = messages;
|
renderer.messages = messages;
|
||||||
|
renderer.pinned_messages = pinned_messages;
|
||||||
renderer.atTop = messages.length < 50;
|
renderer.atTop = messages.length < 50;
|
||||||
renderer.atBottom = true;
|
renderer.atBottom = true;
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { PageHeader } from "../../components/ui/Header";
|
|||||||
import { useClient } from "../../controllers/client/ClientController";
|
import { useClient } from "../../controllers/client/ClientController";
|
||||||
import ChannelHeader from "./ChannelHeader";
|
import ChannelHeader from "./ChannelHeader";
|
||||||
import { MessageArea } from "./messaging/MessageArea";
|
import { MessageArea } from "./messaging/MessageArea";
|
||||||
|
import PinnedMessage from "../../components/common/messaging/bars/PinnedMessage";
|
||||||
|
|
||||||
const ChannelMain = styled.div.attrs({ "data-component": "channel" })`
|
const ChannelMain = styled.div.attrs({ "data-component": "channel" })`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -98,6 +99,7 @@ export const Channel = observer(
|
|||||||
({ id, server_id }: { id: string; server_id: string }) => {
|
({ id, server_id }: { id: string; server_id: string }) => {
|
||||||
const client = useClient();
|
const client = useClient();
|
||||||
const state = useApplicationState();
|
const state = useApplicationState();
|
||||||
|
|
||||||
if (!client.channels.get(id)) {
|
if (!client.channels.get(id)) {
|
||||||
if (server_id) {
|
if (server_id) {
|
||||||
const server = client.servers.get(server_id);
|
const server = client.servers.get(server_id);
|
||||||
@@ -187,6 +189,7 @@ const TextChannel = observer(({ channel }: { channel: ChannelI }) => {
|
|||||||
<ErrorBoundary section="renderer">
|
<ErrorBoundary section="renderer">
|
||||||
<ChannelContent>
|
<ChannelContent>
|
||||||
<NewMessages channel={channel} last_id={lastId} />
|
<NewMessages channel={channel} last_id={lastId} />
|
||||||
|
<PinnedMessage channel={channel} />
|
||||||
<MessageArea channel={channel} last_id={lastId} />
|
<MessageArea channel={channel} last_id={lastId} />
|
||||||
<TypingIndicator channel={channel} />
|
<TypingIndicator channel={channel} />
|
||||||
<JumpToBottom channel={channel} />
|
<JumpToBottom channel={channel} />
|
||||||
|
|||||||
@@ -23,11 +23,12 @@ import { internalEmit, internalSubscribe } from "../../../lib/eventEmitter";
|
|||||||
import { getRenderer } from "../../../lib/renderer/Singleton";
|
import { getRenderer } from "../../../lib/renderer/Singleton";
|
||||||
import { ScrollState } from "../../../lib/renderer/types";
|
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 RequiresOnline from "../../../controllers/client/jsx/RequiresOnline";
|
||||||
import { modalController } from "../../../controllers/modals/ModalController";
|
import { modalController } from "../../../controllers/modals/ModalController";
|
||||||
import ConversationStart from "./ConversationStart";
|
import ConversationStart from "./ConversationStart";
|
||||||
import MessageRenderer from "./MessageRenderer";
|
import MessageRenderer from "./MessageRenderer";
|
||||||
|
import { Message } from "revolt.js/esm";
|
||||||
|
|
||||||
const Area = styled.div.attrs({ "data-scroll-offset": "with-padding" })`
|
const Area = styled.div.attrs({ "data-scroll-offset": "with-padding" })`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -154,14 +155,42 @@ export const MessageArea = observer(({ last_id, channel }: Props) => {
|
|||||||
|
|
||||||
const atTop = (offset = 0) =>
|
const atTop = (offset = 0) =>
|
||||||
ref.current ? ref.current.scrollTop <= offset : false;
|
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.
|
// ? Handle global jump to bottom, e.g. when editing last message in chat.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
||||||
return internalSubscribe("MessageArea", "jump_to_bottom", () =>
|
return internalSubscribe("MessageArea", "jump_to_bottom", () =>
|
||||||
setScrollState({ type: "ScrollToBottom" }),
|
setScrollState({ type: "ScrollToBottom" }),
|
||||||
);
|
);
|
||||||
}, [setScrollState]);
|
}, [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.
|
// ? Handle events from renderer.
|
||||||
useLayoutEffect(
|
useLayoutEffect(
|
||||||
() => setScrollState(renderer.scrollState),
|
() => setScrollState(renderer.scrollState),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { useClient } from "../../../controllers/client/ClientController";
|
|||||||
import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline";
|
import RequiresOnline from "../../../controllers/client/jsx/RequiresOnline";
|
||||||
import ConversationStart from "./ConversationStart";
|
import ConversationStart from "./ConversationStart";
|
||||||
import MessageEditor from "./MessageEditor";
|
import MessageEditor from "./MessageEditor";
|
||||||
|
import { PinMessageBox } from "../../../components/common/messaging/PinMessageBox";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
last_id?: string;
|
last_id?: string;
|
||||||
@@ -150,8 +151,9 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
|
|||||||
);
|
);
|
||||||
blocked = 0;
|
blocked = 0;
|
||||||
}
|
}
|
||||||
|
let lastPinned = null
|
||||||
|
|
||||||
for (const message of renderer.messages) {
|
for (const [i, message] of renderer.messages.entries()) {
|
||||||
if (previous) {
|
if (previous) {
|
||||||
compare(
|
compare(
|
||||||
message._id,
|
message._id,
|
||||||
@@ -162,8 +164,21 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
|
|||||||
previous.masquerade,
|
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(
|
render.push(
|
||||||
<SystemMessage
|
<SystemMessage
|
||||||
key={message._id}
|
key={message._id}
|
||||||
@@ -171,7 +186,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
|
|||||||
attachContext
|
attachContext
|
||||||
highlight={highlight === message._id}
|
highlight={highlight === message._id}
|
||||||
/>,
|
/>,
|
||||||
);
|
)
|
||||||
} else if (message.author?.relationship === "Blocked") {
|
} else if (message.author?.relationship === "Blocked") {
|
||||||
blocked++;
|
blocked++;
|
||||||
} else {
|
} else {
|
||||||
@@ -204,6 +219,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
|
|||||||
const nonces = renderer.messages.map((x) => x.nonce);
|
const nonces = renderer.messages.map((x) => x.nonce);
|
||||||
if (renderer.atBottom) {
|
if (renderer.atBottom) {
|
||||||
for (const msg of queue.get(renderer.channel._id)) {
|
for (const msg of queue.get(renderer.channel._id)) {
|
||||||
|
|
||||||
if (nonces.includes(msg.id)) continue;
|
if (nonces.includes(msg.id)) continue;
|
||||||
|
|
||||||
if (previous) {
|
if (previous) {
|
||||||
@@ -222,6 +238,7 @@ export default observer(({ last_id, renderer, highlight }: Props) => {
|
|||||||
} as MessageI;
|
} as MessageI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render.push(
|
render.push(
|
||||||
<Message
|
<Message
|
||||||
message={
|
message={
|
||||||
|
|||||||
Reference in New Issue
Block a user