commit
29f8668a32
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: "none" }
|
||||||
| { type: "attached"; files: File[] }
|
| { type: "attached"; files: File[] }
|
||||||
| {
|
| {
|
||||||
type: "uploading";
|
type: "uploading";
|
||||||
files: File[];
|
files: File[];
|
||||||
percent: number;
|
percent: number;
|
||||||
cancel: CancelTokenSource;
|
cancel: CancelTokenSource;
|
||||||
}
|
}
|
||||||
| { type: "sending"; files: File[] }
|
| { type: "sending"; files: File[] }
|
||||||
| { type: "failed"; files: File[]; error: string };
|
| { type: "failed"; files: File[]; error: string };
|
||||||
|
|
||||||
|
|
@ -259,7 +259,7 @@ export default observer(({ channel }: Props) => {
|
||||||
}
|
}
|
||||||
console.log(channel) //|| channel.channel_type != "DirectMessage"
|
console.log(channel) //|| channel.channel_type != "DirectMessage"
|
||||||
if (channel.channel_type != "SavedMessages")
|
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 (
|
return (
|
||||||
<Base>
|
<Base>
|
||||||
|
|
@ -299,9 +299,9 @@ export default observer(({ channel }: Props) => {
|
||||||
const text =
|
const text =
|
||||||
action === "quote"
|
action === "quote"
|
||||||
? `${content
|
? `${content
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.map((x) => `> ${x}`)
|
.map((x) => `> ${x}`)
|
||||||
.join("\n")}\n\n`
|
.join("\n")}\n\n`
|
||||||
: `${content} `;
|
: `${content} `;
|
||||||
|
|
||||||
if (!state.draft.has(channel._id)) {
|
if (!state.draft.has(channel._id)) {
|
||||||
|
|
@ -355,8 +355,8 @@ export default observer(({ channel }: Props) => {
|
||||||
toReplace == ""
|
toReplace == ""
|
||||||
? msg.content.toString() + newText
|
? msg.content.toString() + newText
|
||||||
: msg.content
|
: msg.content
|
||||||
.toString()
|
.toString()
|
||||||
.replace(new RegExp(toReplace, flags), newText);
|
.replace(new RegExp(toReplace, flags), newText);
|
||||||
|
|
||||||
if (newContent != msg.content) {
|
if (newContent != msg.content) {
|
||||||
if (newContent.length == 0) {
|
if (newContent.length == 0) {
|
||||||
|
|
@ -434,10 +434,10 @@ export default observer(({ channel }: Props) => {
|
||||||
files,
|
files,
|
||||||
percent: Math.round(
|
percent: Math.round(
|
||||||
(i * 100 + (100 * e.loaded) / e.total) /
|
(i * 100 + (100 * e.loaded) / e.total) /
|
||||||
Math.min(
|
Math.min(
|
||||||
files.length,
|
files.length,
|
||||||
CAN_UPLOAD_AT_ONCE,
|
CAN_UPLOAD_AT_ONCE,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cancel,
|
cancel,
|
||||||
}),
|
}),
|
||||||
|
|
@ -630,42 +630,42 @@ export default observer(({ channel }: Props) => {
|
||||||
</FloatingLayer>
|
</FloatingLayer>
|
||||||
<Base>
|
<Base>
|
||||||
{/* {channel.havePermission("UploadFiles") ? ( */}
|
{/* {channel.havePermission("UploadFiles") ? ( */}
|
||||||
<FileAction>
|
<FileAction>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
size={24}
|
size={24}
|
||||||
behaviour="multi"
|
behaviour="multi"
|
||||||
style="attachment"
|
style="attachment"
|
||||||
fileType="attachments"
|
fileType="attachments"
|
||||||
maxFileSize={20_000_000}
|
maxFileSize={20_000_000}
|
||||||
attached={uploadState.type !== "none"}
|
attached={uploadState.type !== "none"}
|
||||||
uploading={
|
uploading={
|
||||||
uploadState.type === "uploading" ||
|
uploadState.type === "uploading" ||
|
||||||
uploadState.type === "sending"
|
uploadState.type === "sending"
|
||||||
}
|
}
|
||||||
remove={async () =>
|
remove={async () =>
|
||||||
setUploadState({ type: "none" })
|
setUploadState({ type: "none" })
|
||||||
}
|
}
|
||||||
onChange={(files) =>
|
onChange={(files) =>
|
||||||
setUploadState({ type: "attached", files })
|
setUploadState({ type: "attached", files })
|
||||||
}
|
}
|
||||||
cancel={() =>
|
cancel={() =>
|
||||||
uploadState.type === "uploading" &&
|
uploadState.type === "uploading" &&
|
||||||
uploadState.cancel.cancel("cancel")
|
uploadState.cancel.cancel("cancel")
|
||||||
}
|
}
|
||||||
append={(files) => {
|
append={(files) => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
|
|
||||||
if (uploadState.type === "none") {
|
if (uploadState.type === "none") {
|
||||||
setUploadState({ type: "attached", files });
|
setUploadState({ type: "attached", files });
|
||||||
} else if (uploadState.type === "attached") {
|
} else if (uploadState.type === "attached") {
|
||||||
setUploadState({
|
setUploadState({
|
||||||
type: "attached",
|
type: "attached",
|
||||||
files: [...uploadState.files, ...files],
|
files: [...uploadState.files, ...files],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FileAction>
|
</FileAction>
|
||||||
{/* ) : (
|
{/* ) : (
|
||||||
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
|
<ThisCodeWillBeReplacedAnywaysSoIMightAsWellJustDoItThisWay__Padding />
|
||||||
)} */}
|
)} */}
|
||||||
|
|
@ -728,13 +728,13 @@ export default observer(({ channel }: Props) => {
|
||||||
placeholder={
|
placeholder={
|
||||||
channel.channel_type === "DirectMessage"
|
channel.channel_type === "DirectMessage"
|
||||||
? translate("app.main.channel.message_who", {
|
? translate("app.main.channel.message_who", {
|
||||||
person: channel.recipient?.username,
|
person: channel.recipient?.username,
|
||||||
})
|
})
|
||||||
: channel.channel_type === "SavedMessages"
|
: channel.channel_type === "SavedMessages"
|
||||||
? translate("app.main.channel.message_saved")
|
? translate("app.main.channel.message_saved")
|
||||||
: translate("app.main.channel.message_where", {
|
: translate("app.main.channel.message_where", {
|
||||||
channel_name: channel.name ?? undefined,
|
channel_name: channel.name ?? undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
uploadState.type === "uploading" ||
|
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">) {
|
}: 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 }
|
||||||
|
|
@ -87,32 +89,32 @@ type Action =
|
||||||
| { action: "create_channel"; target: Server }
|
| { action: "create_channel"; target: Server }
|
||||||
| { action: "create_category"; target: Server }
|
| { action: "create_category"; target: Server }
|
||||||
| {
|
| {
|
||||||
action: "create_invite";
|
action: "create_invite";
|
||||||
target: Channel;
|
target: Channel;
|
||||||
}
|
}
|
||||||
| { action: "leave_group"; target: Channel }
|
| { action: "leave_group"; target: Channel }
|
||||||
| {
|
| {
|
||||||
action: "delete_channel";
|
action: "delete_channel";
|
||||||
target: Channel;
|
target: Channel;
|
||||||
}
|
}
|
||||||
| { action: "close_dm"; target: Channel }
|
| { action: "close_dm"; target: Channel }
|
||||||
| { action: "leave_server"; target: Server }
|
| { action: "leave_server"; target: Server }
|
||||||
| { action: "delete_server"; target: Server }
|
| { action: "delete_server"; target: Server }
|
||||||
| { action: "edit_identity"; target: Member }
|
| { action: "edit_identity"; target: Member }
|
||||||
| {
|
| {
|
||||||
action: "open_notification_options";
|
action: "open_notification_options";
|
||||||
channel?: Channel;
|
channel?: Channel;
|
||||||
server?: Server;
|
server?: Server;
|
||||||
}
|
}
|
||||||
| { action: "open_settings" }
|
| { action: "open_settings" }
|
||||||
| { action: "open_channel_settings"; id: string }
|
| { action: "open_channel_settings"; id: string }
|
||||||
| { action: "open_server_settings"; id: string }
|
| { action: "open_server_settings"; id: string }
|
||||||
| { action: "open_server_channel_settings"; server: string; id: string }
|
| { action: "open_server_channel_settings"; server: string; id: string }
|
||||||
| {
|
| {
|
||||||
action: "set_notification_state";
|
action: "set_notification_state";
|
||||||
key: string;
|
key: string;
|
||||||
state?: NotificationState;
|
state?: NotificationState;
|
||||||
}
|
}
|
||||||
| { action: "report"; target: User | Server | Message; messageId?: string };
|
| { action: "report"; target: User | Server | Message; messageId?: string };
|
||||||
|
|
||||||
// ! FIXME: I dare someone to re-write this
|
// ! FIXME: I dare someone to re-write this
|
||||||
|
|
@ -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,9 +561,8 @@ 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
|
}`}
|
||||||
}`}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -573,7 +620,7 @@ export default function ContextMenus() {
|
||||||
const user = uid ? client.users.get(uid) : undefined;
|
const user = uid ? client.users.get(uid) : undefined;
|
||||||
const serverChannel =
|
const serverChannel =
|
||||||
targetChannel &&
|
targetChannel &&
|
||||||
(targetChannel.channel_type === "TextChannel")
|
(targetChannel.channel_type === "TextChannel")
|
||||||
? targetChannel
|
? targetChannel
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|
@ -585,8 +632,8 @@ export default function ContextMenus() {
|
||||||
(server
|
(server
|
||||||
? server.permission
|
? server.permission
|
||||||
: serverChannel
|
: serverChannel
|
||||||
? serverChannel.server?.permission
|
? serverChannel.server?.permission
|
||||||
: 0) || 0;
|
: 0) || 0;
|
||||||
const userPermissions = (user ? user.permission : 0) || 0;
|
const userPermissions = (user ? user.permission : 0) || 0;
|
||||||
|
|
||||||
if (unread) {
|
if (unread) {
|
||||||
|
|
@ -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" &&
|
||||||
|
|
@ -880,8 +945,8 @@ export default function ContextMenus() {
|
||||||
type === "Image"
|
type === "Image"
|
||||||
? "open_image"
|
? "open_image"
|
||||||
: type === "Video"
|
: type === "Video"
|
||||||
? "open_video"
|
? "open_video"
|
||||||
: "open_file",
|
: "open_file",
|
||||||
);
|
);
|
||||||
|
|
||||||
generateAction(
|
generateAction(
|
||||||
|
|
@ -892,8 +957,8 @@ export default function ContextMenus() {
|
||||||
type === "Image"
|
type === "Image"
|
||||||
? "save_image"
|
? "save_image"
|
||||||
: type === "Video"
|
: type === "Video"
|
||||||
? "save_video"
|
? "save_video"
|
||||||
: "save_file",
|
: "save_file",
|
||||||
);
|
);
|
||||||
|
|
||||||
generateAction(
|
generateAction(
|
||||||
|
|
@ -929,8 +994,8 @@ export default function ContextMenus() {
|
||||||
type === "Image"
|
type === "Image"
|
||||||
? "open_image"
|
? "open_image"
|
||||||
: type === "Video"
|
: type === "Video"
|
||||||
? "open_video"
|
? "open_video"
|
||||||
: "open_file",
|
: "open_file",
|
||||||
);
|
);
|
||||||
|
|
||||||
generateAction(
|
generateAction(
|
||||||
|
|
@ -941,8 +1006,8 @@ export default function ContextMenus() {
|
||||||
type === "Image"
|
type === "Image"
|
||||||
? "save_image"
|
? "save_image"
|
||||||
: type === "Video"
|
: type === "Video"
|
||||||
? "save_video"
|
? "save_video"
|
||||||
: "save_file",
|
: "save_file",
|
||||||
);
|
);
|
||||||
|
|
||||||
generateAction(
|
generateAction(
|
||||||
|
|
@ -1130,8 +1195,8 @@ export default function ContextMenus() {
|
||||||
type: cid
|
type: cid
|
||||||
? "channel"
|
? "channel"
|
||||||
: message
|
: message
|
||||||
? "message"
|
? "message"
|
||||||
: "user",
|
: "user",
|
||||||
},
|
},
|
||||||
"admin",
|
"admin",
|
||||||
);
|
);
|
||||||
|
|
@ -1158,8 +1223,8 @@ export default function ContextMenus() {
|
||||||
cid
|
cid
|
||||||
? "copy_cid"
|
? "copy_cid"
|
||||||
: message
|
: message
|
||||||
? "copy_mid"
|
? "copy_mid"
|
||||||
: "copy_uid",
|
: "copy_uid",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,9 +99,10 @@ 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);
|
||||||
if (server && server.channel_ids.length > 0) {
|
if (server && server.channel_ids.length > 0) {
|
||||||
let target_id = server.channel_ids[0];
|
let target_id = server.channel_ids[0];
|
||||||
const last_id = state.layout.getLastOpened(server_id);
|
const last_id = state.layout.getLastOpened(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%;
|
||||||
|
|
@ -115,8 +116,8 @@ export const MessageArea = observer(({ last_id, channel }: Props) => {
|
||||||
101,
|
101,
|
||||||
ref.current
|
ref.current
|
||||||
? ref.current.scrollTop +
|
? ref.current.scrollTop +
|
||||||
(ref.current.scrollHeight -
|
(ref.current.scrollHeight -
|
||||||
scrollState.current.previousHeight)
|
scrollState.current.previousHeight)
|
||||||
: 101,
|
: 101,
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
|
@ -148,20 +149,48 @@ export const MessageArea = observer(({ last_id, channel }: Props) => {
|
||||||
const atBottom = (offset = 0) =>
|
const atBottom = (offset = 0) =>
|
||||||
ref.current
|
ref.current
|
||||||
? Math.floor(ref.current?.scrollHeight - ref.current?.scrollTop) -
|
? Math.floor(ref.current?.scrollHeight - ref.current?.scrollTop) -
|
||||||
offset <=
|
offset <=
|
||||||
ref.current?.clientHeight
|
ref.current?.clientHeight
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
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={
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue