mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
support for post visibility
This commit is contained in:
parent
21441ef11a
commit
db14fd0c94
@ -25,11 +25,38 @@ const DEFAULT_POST_POLICY = {
|
||||
"image/bmp",
|
||||
"video/mp4",
|
||||
"video/webm",
|
||||
"video/quicktime"
|
||||
"video/quicktime",
|
||||
],
|
||||
maximunFilesPerRequest: 10
|
||||
maximunFilesPerRequest: 10,
|
||||
}
|
||||
|
||||
const VisibilityOptionLabel = ({ label, icon }) => (
|
||||
<div
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: "10px",
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
|
||||
const visibilityOptions = [
|
||||
{
|
||||
value: "public",
|
||||
label: <VisibilityOptionLabel icon={<Icons.FiUser />} label="Public" />,
|
||||
},
|
||||
{
|
||||
value: "private",
|
||||
label: (
|
||||
<VisibilityOptionLabel icon={<Icons.FiEyeOff />} label="Private" />
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export default class PostCreator extends React.Component {
|
||||
state = {
|
||||
pending: [],
|
||||
@ -39,11 +66,12 @@ export default class PostCreator extends React.Component {
|
||||
postMessage: "",
|
||||
postAttachments: [],
|
||||
postPoll: null,
|
||||
postVisibility: "public",
|
||||
|
||||
fileList: [],
|
||||
postingPolicy: DEFAULT_POST_POLICY,
|
||||
|
||||
mentionsLoadedData: []
|
||||
mentionsLoadedData: [],
|
||||
}
|
||||
|
||||
pollRef = React.createRef()
|
||||
@ -54,7 +82,7 @@ export default class PostCreator extends React.Component {
|
||||
this.setState({
|
||||
postMessage: "",
|
||||
postAttachments: [],
|
||||
fileList: []
|
||||
fileList: [],
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,14 +94,17 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
uploaderVisible: to
|
||||
uploaderVisible: to,
|
||||
})
|
||||
}
|
||||
|
||||
canSubmit = () => {
|
||||
const { postMessage, postAttachments, pending, postingPolicy } = this.state
|
||||
const { postMessage, postAttachments, pending, postingPolicy } =
|
||||
this.state
|
||||
|
||||
const messageLengthValid = postMessage.length !== 0 && postMessage.length < postingPolicy.maxMessageLength
|
||||
const messageLengthValid =
|
||||
postMessage.length !== 0 &&
|
||||
postMessage.length < postingPolicy.maxMessageLength
|
||||
|
||||
if (pending.length !== 0) {
|
||||
return false
|
||||
@ -97,7 +128,7 @@ export default class PostCreator extends React.Component {
|
||||
|
||||
await this.setState({
|
||||
loading: true,
|
||||
uploaderVisible: false
|
||||
uploaderVisible: false,
|
||||
})
|
||||
|
||||
const { postMessage, postAttachments } = this.state
|
||||
@ -106,6 +137,7 @@ export default class PostCreator extends React.Component {
|
||||
message: postMessage,
|
||||
attachments: postAttachments,
|
||||
timestamp: DateTime.local().toISO(),
|
||||
visibility: this.state.postVisibility,
|
||||
}
|
||||
|
||||
if (this.pollRef.current) {
|
||||
@ -121,14 +153,17 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
|
||||
if (this.props.edit_post) {
|
||||
response = await PostModel.update(this.props.edit_post, payload).catch(error => {
|
||||
response = await PostModel.update(
|
||||
this.props.edit_post,
|
||||
payload,
|
||||
).catch((error) => {
|
||||
console.error(error)
|
||||
antd.message.error(error)
|
||||
|
||||
return false
|
||||
})
|
||||
} else {
|
||||
response = await PostModel.create(payload).catch(error => {
|
||||
response = await PostModel.create(payload).catch((error) => {
|
||||
console.error(error)
|
||||
antd.message.error(error)
|
||||
|
||||
@ -137,7 +172,7 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false
|
||||
loading: false,
|
||||
})
|
||||
|
||||
if (response) {
|
||||
@ -160,8 +195,9 @@ export default class PostCreator extends React.Component {
|
||||
uploadFile = async (req) => {
|
||||
this.toggleUploaderVisibility(false)
|
||||
|
||||
const request = await app.cores.remoteStorage.uploadFile(req.file)
|
||||
.catch(error => {
|
||||
const request = await app.cores.remoteStorage
|
||||
.uploadFile(req.file)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
antd.message.error(error)
|
||||
|
||||
@ -179,20 +215,24 @@ export default class PostCreator extends React.Component {
|
||||
|
||||
removeAttachment = (file_uid) => {
|
||||
this.setState({
|
||||
postAttachments: this.state.postAttachments.filter((file) => file.uid !== file_uid),
|
||||
fileList: this.state.fileList.filter((file) => file.uid !== file_uid)
|
||||
postAttachments: this.state.postAttachments.filter(
|
||||
(file) => file.uid !== file_uid,
|
||||
),
|
||||
fileList: this.state.fileList.filter(
|
||||
(file) => file.uid !== file_uid,
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
addAttachment = (file) => {
|
||||
if (Array.isArray(file)) {
|
||||
return this.setState({
|
||||
postAttachments: [...this.state.postAttachments, ...file]
|
||||
postAttachments: [...this.state.postAttachments, ...file],
|
||||
})
|
||||
}
|
||||
|
||||
return this.setState({
|
||||
postAttachments: [...this.state.postAttachments, file]
|
||||
postAttachments: [...this.state.postAttachments, file],
|
||||
})
|
||||
}
|
||||
|
||||
@ -215,7 +255,7 @@ export default class PostCreator extends React.Component {
|
||||
onUploaderChange = (change) => {
|
||||
if (this.state.fileList !== change.fileList) {
|
||||
this.setState({
|
||||
fileList: change.fileList
|
||||
fileList: change.fileList,
|
||||
})
|
||||
}
|
||||
|
||||
@ -224,7 +264,7 @@ export default class PostCreator extends React.Component {
|
||||
this.toggleUploaderVisibility(false)
|
||||
|
||||
this.setState({
|
||||
pending: [...this.state.pending, change.file.uid]
|
||||
pending: [...this.state.pending, change.file.uid],
|
||||
})
|
||||
|
||||
this.uploaderScrollToEnd()
|
||||
@ -234,7 +274,9 @@ export default class PostCreator extends React.Component {
|
||||
case "done": {
|
||||
// remove pending file
|
||||
this.setState({
|
||||
pending: this.state.pending.filter(uid => uid !== change.file.uid)
|
||||
pending: this.state.pending.filter(
|
||||
(uid) => uid !== change.file.uid,
|
||||
),
|
||||
})
|
||||
|
||||
if (Array.isArray(change.file.response.files)) {
|
||||
@ -253,7 +295,9 @@ export default class PostCreator extends React.Component {
|
||||
case "error": {
|
||||
// remove pending file
|
||||
this.setState({
|
||||
pending: this.state.pending.filter(uid => uid !== change.file.uid)
|
||||
pending: this.state.pending.filter(
|
||||
(uid) => uid !== change.file.uid,
|
||||
),
|
||||
})
|
||||
|
||||
// remove file from list
|
||||
@ -272,7 +316,7 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
|
||||
this.setState({
|
||||
postMessage: inputText
|
||||
postMessage: inputText,
|
||||
})
|
||||
}
|
||||
|
||||
@ -294,23 +338,22 @@ export default class PostCreator extends React.Component {
|
||||
const results = await SearchModel.userSearch(`username:${value}`)
|
||||
|
||||
this.setState({
|
||||
mentionsLoadedData: results
|
||||
mentionsLoadedData: results,
|
||||
})
|
||||
|
||||
}, 300)
|
||||
|
||||
updateFileList = (uid, newValue) => {
|
||||
let updatedFileList = this.state.fileList
|
||||
|
||||
// find the file in the list
|
||||
const index = updatedFileList.findIndex(file => file.uid === uid)
|
||||
const index = updatedFileList.findIndex((file) => file.uid === uid)
|
||||
|
||||
// update the file
|
||||
updatedFileList[index] = newValue
|
||||
|
||||
// update the state
|
||||
this.setState({
|
||||
fileList: updatedFileList
|
||||
fileList: updatedFileList,
|
||||
})
|
||||
|
||||
return updatedFileList
|
||||
@ -329,7 +372,9 @@ export default class PostCreator extends React.Component {
|
||||
app.cores.notifications.new({
|
||||
type: "error",
|
||||
title: `Invalid format (${file.type})`,
|
||||
message: "Only the following file formats are allowed: " + this.state.postingPolicy.acceptedMimeTypes.join(", ")
|
||||
message:
|
||||
"Only the following file formats are allowed: " +
|
||||
this.state.postingPolicy.acceptedMimeTypes.join(", "),
|
||||
})
|
||||
return null
|
||||
throw new Error(`Invalid file format`)
|
||||
@ -355,7 +400,7 @@ export default class PostCreator extends React.Component {
|
||||
|
||||
this.onUploaderChange({
|
||||
file: file,
|
||||
fileList: this.updateFileList(file.uid, file)
|
||||
fileList: this.updateFileList(file.uid, file),
|
||||
})
|
||||
},
|
||||
onError: (error) => {
|
||||
@ -364,9 +409,9 @@ export default class PostCreator extends React.Component {
|
||||
|
||||
this.onUploaderChange({
|
||||
file: file,
|
||||
fileList: this.updateFileList(file.uid, file)
|
||||
fileList: this.updateFileList(file.uid, file),
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return file
|
||||
@ -375,7 +420,9 @@ export default class PostCreator extends React.Component {
|
||||
handlePaste = async ({ clipboardData }) => {
|
||||
if (clipboardData && clipboardData.items.length > 0) {
|
||||
// check if the clipboard contains a file
|
||||
const hasFile = Array.from(clipboardData.items).some(item => item.kind === "file")
|
||||
const hasFile = Array.from(clipboardData.items).some(
|
||||
(item) => item.kind === "file",
|
||||
)
|
||||
|
||||
if (!hasFile) {
|
||||
return false
|
||||
@ -384,12 +431,17 @@ export default class PostCreator extends React.Component {
|
||||
for (let index = 0; index < clipboardData.items.length; index++) {
|
||||
const item = clipboardData.items[index]
|
||||
|
||||
let file = await clipboardEventFileToFile(item).catch((error) => {
|
||||
let file = await clipboardEventFileToFile(item).catch(
|
||||
(error) => {
|
||||
console.error(error)
|
||||
app.message.error(`Failed to upload file:`, error.message)
|
||||
app.message.error(
|
||||
`Failed to upload file:`,
|
||||
error.message,
|
||||
)
|
||||
|
||||
return false
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
this.handleManualUpload(file).catch((error) => {
|
||||
console.error(error)
|
||||
@ -399,6 +451,10 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleVisibilityChange = (key) => {
|
||||
this.setState({ postVisibility: key })
|
||||
}
|
||||
|
||||
renderUploadPreviewItem = (item, file, list, actions) => {
|
||||
const uploading = file.status === "uploading"
|
||||
|
||||
@ -406,28 +462,29 @@ export default class PostCreator extends React.Component {
|
||||
this.removeAttachment(file.uid)
|
||||
}
|
||||
|
||||
return <div className={classnames("file", { ["uploading"]: uploading })}>
|
||||
return (
|
||||
<div className={classnames("file", { ["uploading"]: uploading })}>
|
||||
<div className="preview">
|
||||
<img src={file.thumbUrl ?? "/assets/new_file.png"} />
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
{
|
||||
uploading && <Icons.LoadingOutlined style={{ margin: "0 !important" }} />
|
||||
}
|
||||
{
|
||||
!uploading && <antd.Popconfirm
|
||||
{uploading && (
|
||||
<Icons.LoadingOutlined
|
||||
style={{ margin: "0 !important" }}
|
||||
/>
|
||||
)}
|
||||
{!uploading && (
|
||||
<antd.Popconfirm
|
||||
title="Are you sure you want to delete this file?"
|
||||
onConfirm={onClickDelete}
|
||||
>
|
||||
<antd.Button
|
||||
type="link"
|
||||
icon={<Icons.FiTrash />}
|
||||
/>
|
||||
<antd.Button type="link" icon={<Icons.FiTrash />} />
|
||||
</antd.Popconfirm>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
handleDrag = (event) => {
|
||||
@ -440,7 +497,10 @@ export default class PostCreator extends React.Component {
|
||||
this.toggleUploaderVisibility(true)
|
||||
} else if (event.type === "dragleave") {
|
||||
// check if mouse is over the uploader or outside the creatorRef
|
||||
if (this.state.uploaderVisible && !this.creatorRef.current.contains(event.target)) {
|
||||
if (
|
||||
this.state.uploaderVisible &&
|
||||
!this.creatorRef.current.contains(event.target)
|
||||
) {
|
||||
this.toggleUploaderVisibility(false)
|
||||
}
|
||||
}
|
||||
@ -481,14 +541,14 @@ export default class PostCreator extends React.Component {
|
||||
handleAddPoll = () => {
|
||||
if (!this.state.postPoll) {
|
||||
this.setState({
|
||||
postPoll: []
|
||||
postPoll: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleDeletePoll = () => {
|
||||
this.setState({
|
||||
postPoll: null
|
||||
postPoll: null,
|
||||
})
|
||||
}
|
||||
|
||||
@ -499,7 +559,9 @@ export default class PostCreator extends React.Component {
|
||||
postId: this.props.edit_post,
|
||||
})
|
||||
|
||||
const post = await PostModel.getPost({ post_id: this.props.edit_post })
|
||||
const post = await PostModel.getPost({
|
||||
post_id: this.props.edit_post,
|
||||
})
|
||||
|
||||
await this.setState({
|
||||
loading: false,
|
||||
@ -519,7 +581,8 @@ export default class PostCreator extends React.Component {
|
||||
status: "done",
|
||||
}
|
||||
}),
|
||||
postPoll: post.poll_options
|
||||
postPoll: post.poll_options,
|
||||
postVisibility: post.visibility,
|
||||
})
|
||||
}
|
||||
// fetch the posting policy
|
||||
@ -543,32 +606,39 @@ export default class PostCreator extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { postMessage, fileList, loading, uploaderVisible, postingPolicy } = this.state
|
||||
const {
|
||||
postMessage,
|
||||
fileList,
|
||||
loading,
|
||||
uploaderVisible,
|
||||
postingPolicy,
|
||||
} = this.state
|
||||
|
||||
const editMode = !!this.props.edit_post
|
||||
|
||||
const showHeader = !!this.props.edit_post || this.props.reply_to
|
||||
|
||||
return <div
|
||||
return (
|
||||
<div
|
||||
className={"postCreator"}
|
||||
ref={this.creatorRef}
|
||||
onDragEnter={this.handleDrag}
|
||||
onDragLeave={this.handleDrag}
|
||||
style={this.props.style}
|
||||
>
|
||||
{
|
||||
showHeader && <div className="postCreator-header">
|
||||
{
|
||||
this.props.edit_post && <div className="postCreator-header-indicator">
|
||||
{showHeader && (
|
||||
<div className="postCreator-header">
|
||||
{this.props.edit_post && (
|
||||
<div className="postCreator-header-indicator">
|
||||
<p>
|
||||
<Icons.MdEdit />
|
||||
Editing post
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
{
|
||||
this.props.reply_to && <div className="postCreator-header-indicator">
|
||||
{this.props.reply_to && (
|
||||
<div className="postCreator-header-indicator">
|
||||
<p>
|
||||
<Icons.MdReply />
|
||||
Replaying to
|
||||
@ -578,14 +648,15 @@ export default class PostCreator extends React.Component {
|
||||
post_id={this.props.reply_to}
|
||||
onClick={() => {
|
||||
this.props.close()
|
||||
app.navigation.goToPost(this.props.reply_to)
|
||||
app.navigation.goToPost(
|
||||
this.props.reply_to,
|
||||
)
|
||||
}}
|
||||
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
)}
|
||||
|
||||
<div className="textInput">
|
||||
<div className="avatar">
|
||||
@ -607,10 +678,12 @@ export default class PostCreator extends React.Component {
|
||||
return {
|
||||
key: item.id,
|
||||
value: item.username,
|
||||
label: <>
|
||||
label: (
|
||||
<>
|
||||
<antd.Avatar src={item.avatar} />
|
||||
<span>{item.username}</span>
|
||||
</>,
|
||||
</>
|
||||
),
|
||||
}
|
||||
})}
|
||||
onSearch={this.handleOnMentionSearch}
|
||||
@ -621,12 +694,24 @@ export default class PostCreator extends React.Component {
|
||||
type="primary"
|
||||
disabled={loading || !this.canSubmit()}
|
||||
onClick={this.submit}
|
||||
icon={loading ? <Icons.LoadingOutlined spin /> : (editMode ? <Icons.MdEdit /> : <Icons.FiSend />)}
|
||||
icon={
|
||||
loading ? (
|
||||
<Icons.LoadingOutlined spin />
|
||||
) : editMode ? (
|
||||
<Icons.MdEdit />
|
||||
) : (
|
||||
<Icons.FiSend />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={classnames("uploader", { ["visible"]: uploaderVisible })}>
|
||||
<div
|
||||
className={classnames("uploader", {
|
||||
["visible"]: uploaderVisible,
|
||||
})}
|
||||
>
|
||||
<antd.Upload.Dragger
|
||||
openFileDialogOnClick={false}
|
||||
maxCount={postingPolicy.maximunFilesPerRequest}
|
||||
@ -640,19 +725,24 @@ export default class PostCreator extends React.Component {
|
||||
>
|
||||
<div className="hint">
|
||||
<h3>Drag and drop files here</h3>
|
||||
<span>Max {humanSize.fromBytes(postingPolicy.maximumFileSize)}</span>
|
||||
<span>
|
||||
Max{" "}
|
||||
{humanSize.fromBytes(
|
||||
postingPolicy.maximumFileSize,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</antd.Upload.Dragger>
|
||||
</div>
|
||||
|
||||
{
|
||||
this.state.postPoll && <Poll
|
||||
{this.state.postPoll && (
|
||||
<Poll
|
||||
formRef={this.pollRef}
|
||||
options={this.state.postPoll}
|
||||
onClose={this.handleDeletePoll}
|
||||
editMode
|
||||
/>
|
||||
}
|
||||
)}
|
||||
|
||||
<div className="actions">
|
||||
<antd.Button
|
||||
@ -666,7 +756,16 @@ export default class PostCreator extends React.Component {
|
||||
icon={<Icons.MdPoll />}
|
||||
onClick={this.handleAddPoll}
|
||||
/>
|
||||
|
||||
<antd.Select
|
||||
id="post-visibility"
|
||||
size="small"
|
||||
value={this.state.postVisibility}
|
||||
onChange={this.handleVisibilityChange}
|
||||
options={visibilityOptions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
@ -266,28 +266,35 @@
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
transition: all 150ms ease-in-out;
|
||||
|
||||
opacity: 0;
|
||||
color: rgba(var(--bg_color_1), 1);
|
||||
|
||||
border-radius: @file_preview_borderRadius;
|
||||
flex-direction: inline-flex;
|
||||
|
||||
svg {
|
||||
color: rgba(var(--bg_color_1), 1);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-select {
|
||||
.ant-select-selector {
|
||||
border: 0;
|
||||
padding: 4px 10px !important;
|
||||
background-color: rgba(var(--bg_color_1), 0.8);
|
||||
|
||||
.ant-select-selection-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ant-select-arrow {
|
||||
font-size: 0.6rem;
|
||||
right: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -334,6 +334,8 @@ export class PostsListsComponent extends React.Component {
|
||||
"posts",
|
||||
)
|
||||
})
|
||||
|
||||
app.cores.api.client().sockets.posts.emit("connect_realtime")
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,6 +364,8 @@ export class PostsListsComponent extends React.Component {
|
||||
"posts",
|
||||
)
|
||||
})
|
||||
|
||||
app.cores.api.client().sockets.posts.emit("disconnect_realtime")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,34 +4,38 @@ export default {
|
||||
schema: {
|
||||
user_id: {
|
||||
type: String,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
created_at: {
|
||||
type: Date,
|
||||
required: true
|
||||
required: true,
|
||||
},
|
||||
message: {
|
||||
type: String
|
||||
type: String,
|
||||
},
|
||||
attachments: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
flags: {
|
||||
type: Array,
|
||||
default: []
|
||||
default: [],
|
||||
},
|
||||
reply_to: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
updated_at: {
|
||||
type: String,
|
||||
default: null
|
||||
default: null,
|
||||
},
|
||||
poll_options: {
|
||||
type: Array,
|
||||
default: null
|
||||
}
|
||||
}
|
||||
default: null,
|
||||
},
|
||||
visibility: {
|
||||
type: String,
|
||||
default: "public",
|
||||
},
|
||||
},
|
||||
}
|
@ -6,7 +6,7 @@ export default class Posts {
|
||||
static getSaved = require("./methods/getSaved").default
|
||||
static fromUserId = require("./methods/fromUserId").default
|
||||
static create = require("./methods/create").default
|
||||
static fullfillPost = require("./methods/fullfill").default
|
||||
static stage = require("./methods/stage").default
|
||||
static toggleSave = require("./methods/toggleSave").default
|
||||
static toggleLike = require("./methods/toggleLike").default
|
||||
static report = require("./methods/report").default
|
||||
|
@ -2,18 +2,37 @@ import requiredFields from "@shared-utils/requiredFields"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
import { Post } from "@db_models"
|
||||
import fullfill from "./fullfill"
|
||||
import stage from "./stage"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
const visibilityOptions = ["public", "private", "only_mutuals"]
|
||||
|
||||
export default async (payload = {}, req) => {
|
||||
await requiredFields(["user_id"], payload)
|
||||
|
||||
let { user_id, message, attachments, timestamp, reply_to, poll_options } = payload
|
||||
let {
|
||||
user_id,
|
||||
message,
|
||||
attachments,
|
||||
timestamp,
|
||||
reply_to,
|
||||
poll_options,
|
||||
visibility = "public",
|
||||
} = payload
|
||||
|
||||
// check if visibility is valid
|
||||
if (!visibilityOptions.includes(visibility)) {
|
||||
throw new OperationError(400, "Invalid visibility option")
|
||||
}
|
||||
|
||||
// check if is a Array and have at least one element
|
||||
const isAttachmentArray = Array.isArray(attachments) && attachments.length > 0
|
||||
const isAttachmentArray =
|
||||
Array.isArray(attachments) && attachments.length > 0
|
||||
|
||||
if (!isAttachmentArray && !message) {
|
||||
throw new OperationError(400, "Cannot create a post without message or attachments")
|
||||
throw new OperationError(
|
||||
400,
|
||||
"Cannot create a post without message or attachments",
|
||||
)
|
||||
}
|
||||
|
||||
if (isAttachmentArray) {
|
||||
@ -56,19 +75,40 @@ export default async (payload = {}) => {
|
||||
reply_to: reply_to,
|
||||
flags: [],
|
||||
poll_options: poll_options,
|
||||
visibility: visibility.toLocaleLowerCase(),
|
||||
})
|
||||
|
||||
await post.save()
|
||||
|
||||
post = post.toObject()
|
||||
|
||||
const result = await fullfill({
|
||||
const result = await stage({
|
||||
posts: post,
|
||||
for_user_id: user_id
|
||||
for_user_id: user_id,
|
||||
})
|
||||
|
||||
// broadcast post to all users
|
||||
if (visibility === "public") {
|
||||
global.websocket.io
|
||||
.to("global:posts:realtime")
|
||||
.emit(`post.new`, result[0])
|
||||
}
|
||||
|
||||
if (visibility === "private") {
|
||||
const userSocket = await global.websocket.find.socketByUserId(
|
||||
post.user_id,
|
||||
)
|
||||
|
||||
if (userSocket) {
|
||||
userSocket.emit(`post.new`, result[0])
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create background jobs (nsfw dectection)
|
||||
global.websocket.io.of("/").emit(`post.new`, result[0])
|
||||
global.queues.createJob("classify_post_attachments", {
|
||||
post_id: post._id.toString(),
|
||||
auth_token: req.headers.authorization,
|
||||
})
|
||||
|
||||
return post
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Post } from "@db_models"
|
||||
import fullfillPostsData from "./fullfill"
|
||||
import stage from "./stage"
|
||||
|
||||
const maxLimit = 300
|
||||
|
||||
@ -36,7 +36,7 @@ export default async (payload = {}) => {
|
||||
}
|
||||
|
||||
// fullfill data
|
||||
posts = await fullfillPostsData({
|
||||
posts = await stage({
|
||||
posts,
|
||||
for_user_id,
|
||||
})
|
||||
|
@ -1,14 +1,18 @@
|
||||
import { Post, PostLike, PostSave } from "@db_models"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
let {
|
||||
post_id
|
||||
} = payload
|
||||
let { post_id } = payload
|
||||
|
||||
if (!post_id) {
|
||||
throw new OperationError(400, "Missing post_id")
|
||||
}
|
||||
|
||||
const post = await Post.findById(post_id)
|
||||
|
||||
if (!post) {
|
||||
throw new OperationError(404, "Post not found")
|
||||
}
|
||||
|
||||
await Post.deleteOne({
|
||||
_id: post_id,
|
||||
}).catch((err) => {
|
||||
@ -36,8 +40,24 @@ export default async (payload = {}) => {
|
||||
throw new OperationError(500, `An error has occurred: ${err.message}`)
|
||||
})
|
||||
|
||||
global.websocket.io.of("/").emit(`post.delete`, post_id)
|
||||
global.websocket.io.of("/").emit(`post.delete.${post_id}`, post_id)
|
||||
if (post.visibility === "public") {
|
||||
global.websocket.io
|
||||
.to("global:posts:realtime")
|
||||
.emit(`post.delete`, post)
|
||||
global.websocket.io
|
||||
.to("global:posts:realtime")
|
||||
.emit(`post.delete.${post_id}`, post)
|
||||
}
|
||||
|
||||
if (post.visibility === "private") {
|
||||
const userSocket = await global.websocket.find.socketByUserId(
|
||||
post.user_id,
|
||||
)
|
||||
if (userSocket) {
|
||||
userSocket.emit(`post.delete`, post_id)
|
||||
userSocket.emit(`post.delete.${post_id}`, post_id)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
deleted: true,
|
||||
|
@ -1,140 +0,0 @@
|
||||
import { User, PostLike, PostSave, Post, VotePoll } from "@db_models"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
let {
|
||||
posts,
|
||||
for_user_id,
|
||||
} = payload
|
||||
|
||||
if (!Array.isArray(posts)) {
|
||||
posts = [posts]
|
||||
}
|
||||
|
||||
if (posts.every((post) => !post)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let postsSavesIds = []
|
||||
|
||||
if (for_user_id) {
|
||||
const postsSaves = await PostSave.find({ user_id: for_user_id })
|
||||
.sort({ saved_at: -1 })
|
||||
|
||||
postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
|
||||
}
|
||||
|
||||
const postsIds = posts.map((post) => post._id)
|
||||
const usersIds = posts.map((post) => post.user_id)
|
||||
|
||||
let [usersData, likesData, pollVotes] = await Promise.all([
|
||||
User.find({
|
||||
_id: {
|
||||
$in: usersIds
|
||||
}
|
||||
}).catch(() => { }),
|
||||
PostLike.find({
|
||||
post_id: {
|
||||
$in: postsIds
|
||||
}
|
||||
}).catch(() => []),
|
||||
VotePoll.find({
|
||||
post_id: {
|
||||
$in: postsIds
|
||||
}
|
||||
}).catch(() => [])
|
||||
])
|
||||
|
||||
// wrap likesData by post_id
|
||||
likesData = likesData.reduce((acc, like) => {
|
||||
if (!acc[like.post_id]) {
|
||||
acc[like.post_id] = []
|
||||
}
|
||||
|
||||
acc[like.post_id].push(like)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
posts = await Promise.all(posts.map(async (post, index) => {
|
||||
if (typeof post.toObject === "function") {
|
||||
post = post.toObject()
|
||||
}
|
||||
|
||||
let user = usersData.find((user) => user._id.toString() === post.user_id.toString())
|
||||
|
||||
if (!user) {
|
||||
user = {
|
||||
_deleted: true,
|
||||
username: "Deleted user",
|
||||
}
|
||||
}
|
||||
|
||||
if (post.reply_to) {
|
||||
post.reply_to_data = await Post.findById(post.reply_to)
|
||||
|
||||
if (post.reply_to_data) {
|
||||
post.reply_to_data = post.reply_to_data.toObject()
|
||||
|
||||
const replyUserData = await User.findById(post.reply_to_data.user_id)
|
||||
|
||||
if (replyUserData) {
|
||||
post.reply_to_data.user = replyUserData.toObject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post.hasReplies = await Post.countDocuments({ reply_to: post._id })
|
||||
|
||||
let likes = likesData[post._id.toString()] ?? []
|
||||
|
||||
post.countLikes = likes.length
|
||||
|
||||
const postPollVotes = pollVotes.filter((vote) => {
|
||||
if (vote.post_id !== post._id.toString()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (for_user_id) {
|
||||
post.isLiked = likes.some((like) => like.user_id.toString() === for_user_id)
|
||||
post.isSaved = postsSavesIds.includes(post._id.toString())
|
||||
|
||||
if (Array.isArray(post.poll_options)) {
|
||||
post.poll_options = post.poll_options.map((option) => {
|
||||
option.voted = !!postPollVotes.find((vote) => {
|
||||
if (vote.user_id !== for_user_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (vote.option_id !== option.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
option.count = postPollVotes.filter((vote) => {
|
||||
if (vote.option_id !== option.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}).length
|
||||
|
||||
return option
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
post.share_url = `${process.env.APP_URL}/post/${post._id}`
|
||||
|
||||
return {
|
||||
...post,
|
||||
user,
|
||||
}
|
||||
}))
|
||||
|
||||
return posts
|
||||
}
|
@ -1,13 +1,8 @@
|
||||
import { Post } from "@db_models"
|
||||
import fullfillPostsData from "./fullfill"
|
||||
import stage from "./stage"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
const {
|
||||
post_id,
|
||||
for_user_id,
|
||||
trim = 0,
|
||||
limit = 50,
|
||||
} = payload
|
||||
const { post_id, for_user_id, trim = 0, limit = 50 } = payload
|
||||
|
||||
if (!post_id) {
|
||||
throw new OperationError(400, "Post ID is required")
|
||||
@ -20,7 +15,7 @@ export default async (payload = {}) => {
|
||||
.skip(trim)
|
||||
.sort({ created_at: -1 })
|
||||
|
||||
posts = await fullfillPostsData({
|
||||
posts = await stage({
|
||||
posts,
|
||||
for_user_id,
|
||||
})
|
||||
|
161
packages/server/services/posts/classes/posts/methods/stage.js
Normal file
161
packages/server/services/posts/classes/posts/methods/stage.js
Normal file
@ -0,0 +1,161 @@
|
||||
import { User, PostLike, PostSave, Post, VotePoll } from "@db_models"
|
||||
|
||||
export default async (payload = {}) => {
|
||||
let { posts, for_user_id } = payload
|
||||
|
||||
if (!Array.isArray(posts)) {
|
||||
posts = [posts]
|
||||
}
|
||||
|
||||
if (posts.every((post) => !post)) {
|
||||
return []
|
||||
}
|
||||
|
||||
let postsSavesIds = []
|
||||
|
||||
if (for_user_id) {
|
||||
const postsSaves = await PostSave.find({ user_id: for_user_id }).sort({
|
||||
saved_at: -1,
|
||||
})
|
||||
|
||||
postsSavesIds = postsSaves.map((postSave) => postSave.post_id)
|
||||
}
|
||||
|
||||
const postsIds = posts.map((post) => post._id)
|
||||
const usersIds = posts.map((post) => post.user_id)
|
||||
|
||||
let [usersData, likesData, pollVotes] = await Promise.all([
|
||||
User.find({
|
||||
_id: {
|
||||
$in: usersIds,
|
||||
},
|
||||
}).catch(() => {}),
|
||||
PostLike.find({
|
||||
post_id: {
|
||||
$in: postsIds,
|
||||
},
|
||||
}).catch(() => []),
|
||||
VotePoll.find({
|
||||
post_id: {
|
||||
$in: postsIds,
|
||||
},
|
||||
}).catch(() => []),
|
||||
])
|
||||
|
||||
// wrap likesData by post_id
|
||||
likesData = likesData.reduce((acc, like) => {
|
||||
if (!acc[like.post_id]) {
|
||||
acc[like.post_id] = []
|
||||
}
|
||||
|
||||
acc[like.post_id].push(like)
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
posts = await Promise.all(
|
||||
posts.map(async (post, index) => {
|
||||
if (typeof post.toObject === "function") {
|
||||
post = post.toObject()
|
||||
}
|
||||
|
||||
if (post.visibility === "private" && post.user_id !== for_user_id) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (
|
||||
post.visibility === "only_mutuals" &&
|
||||
post.user_id !== for_user_id
|
||||
) {
|
||||
// TODO
|
||||
return null
|
||||
}
|
||||
|
||||
let user = usersData.find(
|
||||
(user) => user._id.toString() === post.user_id.toString(),
|
||||
)
|
||||
|
||||
if (!user) {
|
||||
user = {
|
||||
_deleted: true,
|
||||
username: "Deleted user",
|
||||
}
|
||||
}
|
||||
|
||||
if (post.reply_to) {
|
||||
post.reply_to_data = await Post.findById(post.reply_to)
|
||||
|
||||
if (post.reply_to_data) {
|
||||
post.reply_to_data = post.reply_to_data.toObject()
|
||||
|
||||
const replyUserData = await User.findById(
|
||||
post.reply_to_data.user_id,
|
||||
)
|
||||
|
||||
if (replyUserData) {
|
||||
post.reply_to_data.user = replyUserData.toObject()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
post.hasReplies = await Post.countDocuments({ reply_to: post._id })
|
||||
|
||||
let likes = likesData[post._id.toString()] ?? []
|
||||
|
||||
post.countLikes = likes.length
|
||||
|
||||
const postPollVotes = pollVotes.filter((vote) => {
|
||||
if (vote.post_id !== post._id.toString()) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
if (for_user_id) {
|
||||
post.isLiked = likes.some(
|
||||
(like) => like.user_id.toString() === for_user_id,
|
||||
)
|
||||
post.isSaved = postsSavesIds.includes(post._id.toString())
|
||||
|
||||
if (Array.isArray(post.poll_options)) {
|
||||
post.poll_options = post.poll_options.map((option) => {
|
||||
option.voted = !!postPollVotes.find((vote) => {
|
||||
if (vote.user_id !== for_user_id) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (vote.option_id !== option.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
option.count = postPollVotes.filter((vote) => {
|
||||
if (vote.option_id !== option.id) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}).length
|
||||
|
||||
return option
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
post.share_url = `${process.env.APP_URL}/post/${post._id}`
|
||||
|
||||
return {
|
||||
...post,
|
||||
user,
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// clear undefined and null
|
||||
posts = posts.filter((post) => post !== undefined && post !== null)
|
||||
|
||||
return posts
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { Post } from "@db_models"
|
||||
import { DateTime } from "luxon"
|
||||
import fullfill from "./fullfill"
|
||||
import stage from "./stage"
|
||||
|
||||
export default async (post_id, update) => {
|
||||
let post = await Post.findById(post_id)
|
||||
@ -31,12 +31,30 @@ export default async (post_id, update) => {
|
||||
|
||||
post = post.toObject()
|
||||
|
||||
const result = await fullfill({
|
||||
const result = await stage({
|
||||
posts: post,
|
||||
for_user_id: post.user_id,
|
||||
})
|
||||
|
||||
global.websocket.io.of("/").emit(`post.update`, result[0])
|
||||
global.websocket.io.of("/").emit(`post.update.${post_id}`, result[0])
|
||||
if (post.visibility === "public") {
|
||||
global.websocket.io
|
||||
.to("global:posts:realtime")
|
||||
.emit(`post.update`, result[0])
|
||||
global.websocket.io
|
||||
.to("global:posts:realtime")
|
||||
.emit(`post.update.${post_id}`, result[0])
|
||||
}
|
||||
|
||||
if (post.visibility === "private") {
|
||||
const userSocket = await global.websocket.find.socketByUserId(
|
||||
post.user_id,
|
||||
)
|
||||
|
||||
if (userSocket) {
|
||||
userSocket.emit(`post.update`, result[0])
|
||||
userSocket.emit(`post.update.${post_id}`, result[0])
|
||||
}
|
||||
}
|
||||
|
||||
return result[0]
|
||||
}
|
@ -2,27 +2,69 @@ import { Server } from "linebridge"
|
||||
|
||||
import DbManager from "@shared-classes/DbManager"
|
||||
import RedisClient from "@shared-classes/RedisClient"
|
||||
import TaskQueueManager from "@shared-classes/TaskQueueManager"
|
||||
|
||||
import SharedMiddlewares from "@shared-middlewares"
|
||||
|
||||
// wsfast
|
||||
import HyperExpress from "hyper-express"
|
||||
|
||||
class WSFastServer {
|
||||
router = new HyperExpress.Router()
|
||||
|
||||
clients = new Set()
|
||||
|
||||
routes = {
|
||||
connect: async (socket) => {
|
||||
console.log("Client connected", socket)
|
||||
},
|
||||
}
|
||||
|
||||
async initialize(engine) {
|
||||
this.engine = engine
|
||||
|
||||
Object.keys(this.routes).forEach((route) => {
|
||||
this.router.ws(`/${route}`, this.routes[route])
|
||||
})
|
||||
|
||||
this.engine.app.use(this.router)
|
||||
}
|
||||
}
|
||||
|
||||
export default class API extends Server {
|
||||
static refName = "posts"
|
||||
static enableWebsockets = true
|
||||
static routesPath = `${__dirname}/routes`
|
||||
static wsRoutesPath = `${__dirname}/routes_ws`
|
||||
|
||||
static listen_port = process.env.HTTP_LISTEN_PORT ?? 3001
|
||||
|
||||
middlewares = {
|
||||
...SharedMiddlewares
|
||||
...SharedMiddlewares,
|
||||
}
|
||||
|
||||
contexts = {
|
||||
db: new DbManager(),
|
||||
redis: RedisClient(),
|
||||
ws: new WSFastServer(this.engine),
|
||||
}
|
||||
|
||||
queuesManager = new TaskQueueManager(
|
||||
{
|
||||
workersPath: `${__dirname}/queues`,
|
||||
},
|
||||
this,
|
||||
)
|
||||
|
||||
async onInitialize() {
|
||||
await this.contexts.db.initialize()
|
||||
await this.contexts.redis.initialize()
|
||||
await this.queuesManager.initialize({
|
||||
redisOptions: this.engine.ws.redis.options,
|
||||
})
|
||||
await this.contexts.ws.initialize(this.engine)
|
||||
|
||||
global.queues = this.queuesManager
|
||||
}
|
||||
|
||||
handleWsAuth = require("@shared-lib/handleWsAuth").default
|
||||
|
@ -0,0 +1,67 @@
|
||||
import { Post } from "@db_models"
|
||||
import axios from "axios"
|
||||
|
||||
const classifyAPI = "https://vision-service.ragestudio.net"
|
||||
|
||||
const adultLevels = [
|
||||
"VERY_UNLIKELY",
|
||||
"UNLIKELY",
|
||||
"POSSIBLE",
|
||||
"LIKELY",
|
||||
"VERY_LIKELY",
|
||||
]
|
||||
|
||||
export default {
|
||||
id: "classify_post_attachments",
|
||||
maxJobs: 100,
|
||||
process: async (job) => {
|
||||
const { post_id, auth_token } = job.data
|
||||
|
||||
let post = await Post.findById(post_id).lean()
|
||||
|
||||
console.log(`[CLASSIFY] Checking post ${post_id}`)
|
||||
|
||||
if (!post) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!Array.isArray(post.attachments)) {
|
||||
return false
|
||||
}
|
||||
|
||||
for await (const attachment of post.attachments) {
|
||||
if (!attachment.url) {
|
||||
continue
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: "GET",
|
||||
url: `${classifyAPI}/safe_detect`,
|
||||
headers: {
|
||||
Authorization: auth_token,
|
||||
},
|
||||
params: {
|
||||
url: attachment.url,
|
||||
},
|
||||
})
|
||||
|
||||
console.log(
|
||||
`[CLASSIFY] Attachment [${attachment.url}] classified as ${response.data.detections.adult}`,
|
||||
)
|
||||
|
||||
const adultLevel = adultLevels.indexOf(
|
||||
response.data.detections.adult,
|
||||
)
|
||||
|
||||
if (!Array.isArray(attachment.flags)) {
|
||||
attachment.flags = []
|
||||
}
|
||||
|
||||
if (adultLevel > 2) {
|
||||
attachment.flags.push("nsfw")
|
||||
}
|
||||
}
|
||||
|
||||
await Post.findByIdAndUpdate(post._id, post)
|
||||
},
|
||||
}
|
@ -1,11 +1,17 @@
|
||||
import PostClass from "@classes/posts"
|
||||
import { Post } from "@db_models"
|
||||
|
||||
const AllowedFields = ["message", "tags", "attachments", "poll_options"]
|
||||
const AllowedFields = [
|
||||
"message",
|
||||
"tags",
|
||||
"attachments",
|
||||
"poll_options",
|
||||
"visibility",
|
||||
]
|
||||
|
||||
// TODO: Get limits from LimitsAPI
|
||||
const MaxStringsLengths = {
|
||||
message: 2000
|
||||
message: 2000,
|
||||
}
|
||||
|
||||
export default {
|
||||
@ -26,10 +32,16 @@ export default {
|
||||
AllowedFields.forEach((key) => {
|
||||
if (typeof req.body[key] !== "undefined") {
|
||||
// check maximung strings length
|
||||
if (typeof req.body[key] === "string" && MaxStringsLengths[key]) {
|
||||
if (
|
||||
typeof req.body[key] === "string" &&
|
||||
MaxStringsLengths[key]
|
||||
) {
|
||||
if (req.body[key].length > MaxStringsLengths[key]) {
|
||||
// create a substring
|
||||
update[key] = req.body[key].substring(0, MaxStringsLengths[key])
|
||||
update[key] = req.body[key].substring(
|
||||
0,
|
||||
MaxStringsLengths[key],
|
||||
)
|
||||
} else {
|
||||
update[key] = req.body[key]
|
||||
}
|
||||
@ -40,5 +52,5 @@ export default {
|
||||
})
|
||||
|
||||
return await PostClass.update(req.params.post_id, update)
|
||||
}
|
||||
},
|
||||
}
|
@ -3,11 +3,14 @@ import Posts from "@classes/posts"
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req, res) => {
|
||||
const result = await Posts.create({
|
||||
const result = await Posts.create(
|
||||
{
|
||||
...req.body,
|
||||
user_id: req.auth.session.user_id,
|
||||
})
|
||||
},
|
||||
req,
|
||||
)
|
||||
|
||||
return result
|
||||
}
|
||||
},
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { Post } from "@db_models"
|
||||
import fullfill from "@classes/posts/methods/fullfill"
|
||||
import stage from "@classes/posts/methods/stage"
|
||||
|
||||
export default {
|
||||
middlewares: ["withOptionalAuthentication"],
|
||||
@ -8,18 +8,18 @@ export default {
|
||||
|
||||
let result = await Post.find({
|
||||
message: {
|
||||
$regex: new RegExp(`#${req.params.trending}`, "gi")
|
||||
}
|
||||
$regex: new RegExp(`#${req.params.trending}`, "gi"),
|
||||
},
|
||||
})
|
||||
.sort({ created_at: -1 })
|
||||
.skip(trim ?? 0)
|
||||
.limit(limit ?? 20)
|
||||
|
||||
result = await fullfill({
|
||||
result = await stage({
|
||||
posts: result,
|
||||
for_user_id: req.auth.session.user_id,
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
},
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export default async function (socket) {
|
||||
console.log(`Socket ${socket.id} connected to realtime posts`)
|
||||
socket.join("global:posts:realtime")
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
export default async function (socket) {
|
||||
console.log(`Socket ${socket.id} disconnected from realtime posts`)
|
||||
socket.leave("global:posts:realtime")
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user