commit
29f8668a32
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,7 @@
|
|||
{
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
"editor.formatOnSave": true,
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 3195d642cd766cb62d34eb2a57ce3a09e775e91f
|
||||
Subproject commit 8ecb9a34b1b459b5280a6351a4044dfa44b68019
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit cd9e84a337c72709b82bb4eca794ec7474a0ee7e
|
||||
Subproject commit 007702579cd6e611fce79498461e89951387108d
|
||||
|
|
@ -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" ||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -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">) {
|
||||
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({
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export class ChannelRenderer {
|
|||
atTop: Nullable<boolean> = null;
|
||||
atBottom: Nullable<boolean> = null;
|
||||
messages: Message[] = [];
|
||||
pinned_messages: Message[] = [];
|
||||
|
||||
currentRenderer: RendererRoutines = SimpleRenderer;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
Loading…
Reference in New Issue