diff --git a/packages/app/src/components/PostCreator/index.jsx b/packages/app/src/components/PostCreator/index.jsx
index cb2b74cc..d0e7d30a 100755
--- a/packages/app/src/components/PostCreator/index.jsx
+++ b/packages/app/src/components/PostCreator/index.jsx
@@ -17,656 +17,755 @@ import SearchModel from "@models/search"
import "./index.less"
const DEFAULT_POST_POLICY = {
- maxMessageLength: 512,
- acceptedMimeTypes: [
- "image/gif",
- "image/png",
- "image/jpeg",
- "image/bmp",
- "video/mp4",
- "video/webm",
- "video/quicktime"
- ],
- maximunFilesPerRequest: 10
+ maxMessageLength: 512,
+ acceptedMimeTypes: [
+ "image/gif",
+ "image/png",
+ "image/jpeg",
+ "image/bmp",
+ "video/mp4",
+ "video/webm",
+ "video/quicktime",
+ ],
+ maximunFilesPerRequest: 10,
}
+const VisibilityOptionLabel = ({ label, icon }) => (
+
+ {icon}
+ {label}
+
+)
+
+const visibilityOptions = [
+ {
+ value: "public",
+ label: } label="Public" />,
+ },
+ {
+ value: "private",
+ label: (
+ } label="Private" />
+ ),
+ },
+]
+
export default class PostCreator extends React.Component {
- state = {
- pending: [],
- loading: false,
- uploaderVisible: false,
-
- postMessage: "",
- postAttachments: [],
- postPoll: null,
-
- fileList: [],
- postingPolicy: DEFAULT_POST_POLICY,
-
- mentionsLoadedData: []
- }
-
- pollRef = React.createRef()
-
- creatorRef = React.createRef()
-
- cleanPostData = () => {
- this.setState({
- postMessage: "",
- postAttachments: [],
- fileList: []
- })
- }
-
- toggleUploaderVisibility = (to) => {
- to = to ?? !this.state.uploaderVisible
-
- if (to === this.state.uploaderVisible) {
- return
- }
-
- this.setState({
- uploaderVisible: to
- })
- }
-
- canSubmit = () => {
- const { postMessage, postAttachments, pending, postingPolicy } = this.state
-
- const messageLengthValid = postMessage.length !== 0 && postMessage.length < postingPolicy.maxMessageLength
-
- if (pending.length !== 0) {
- return false
- }
-
- if (!messageLengthValid && postAttachments.length === 0) {
- return false
- }
-
- return true
- }
-
- submit = lodash.debounce(async () => {
- if (this.state.loading) {
- return false
- }
-
- if (!this.canSubmit()) {
- return false
- }
-
- await this.setState({
- loading: true,
- uploaderVisible: false
- })
-
- const { postMessage, postAttachments } = this.state
-
- const payload = {
- message: postMessage,
- attachments: postAttachments,
- timestamp: DateTime.local().toISO(),
- }
-
- if (this.pollRef.current) {
- let { options } = this.pollRef.current.getFieldsValue()
-
- payload.poll_options = options.filter((option) => !!option.label)
- }
-
- let response = null
-
- if (this.props.reply_to) {
- payload.reply_to = this.props.reply_to
- }
-
- if (this.props.edit_post) {
- 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 => {
- console.error(error)
- antd.message.error(error)
-
- return false
- })
- }
-
- this.setState({
- loading: false
- })
-
- if (response) {
- this.cleanPostData()
-
- if (typeof this.props.onPost === "function") {
- this.props.onPost()
- }
-
- if (typeof this.props.close === "function") {
- this.props.close()
- }
-
- if (this.props.reply_to) {
- app.navigation.goToPost(this.props.reply_to)
- }
- }
- }, 50)
-
- uploadFile = async (req) => {
- this.toggleUploaderVisibility(false)
-
- const request = await app.cores.remoteStorage.uploadFile(req.file)
- .catch(error => {
- console.error(error)
- antd.message.error(error)
-
- req.onError(error)
-
- return false
- })
-
- if (request) {
- console.log(`Upload done >`, request)
-
- return req.onSuccess(request)
- }
- }
-
- 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)
- })
- }
-
- addAttachment = (file) => {
- if (Array.isArray(file)) {
- return this.setState({
- postAttachments: [...this.state.postAttachments, ...file]
- })
- }
-
- return this.setState({
- postAttachments: [...this.state.postAttachments, file]
- })
- }
-
- uploaderScrollToEnd = () => {
- // scroll to max right
- const element = document.querySelector(".ant-upload-list-picture-card")
-
- // calculate the element's width and scroll to the end
- const scrollToLeft = element.scrollWidth - element.clientWidth
-
- if (element) {
- element.scrollTo({
- top: 0,
- left: scrollToLeft,
- behavior: "smooth",
- })
- }
- }
-
- onUploaderChange = (change) => {
- if (this.state.fileList !== change.fileList) {
- this.setState({
- fileList: change.fileList
- })
- }
-
- switch (change.file.status) {
- case "uploading": {
- this.toggleUploaderVisibility(false)
-
- this.setState({
- pending: [...this.state.pending, change.file.uid]
- })
-
- this.uploaderScrollToEnd()
-
- break
- }
- case "done": {
- // remove pending file
- this.setState({
- pending: this.state.pending.filter(uid => uid !== change.file.uid)
- })
-
- if (Array.isArray(change.file.response.files)) {
- change.file.response.files.forEach((file) => {
- this.addAttachment(file)
- })
- } else {
- this.addAttachment(change.file.response)
- }
-
- // scroll to end
- this.uploaderScrollToEnd()
-
- break
- }
- case "error": {
- // remove pending file
- this.setState({
- pending: this.state.pending.filter(uid => uid !== change.file.uid)
- })
-
- // remove file from list
- this.removeAttachment(change.file.uid)
- }
- default: {
- break
- }
- }
- }
-
- handleMessageInputChange = (inputText) => {
- // if the fist character is a space or a whitespace remove it
- if (inputText[0] === " " || inputText[0] === "\n") {
- inputText = inputText.slice(1)
- }
-
- this.setState({
- postMessage: inputText
- })
- }
-
- handleMessageInputKeydown = async (e) => {
- // detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed
- if (e.keyCode === 13 && !e.shiftKey) {
- e.preventDefault()
- e.stopPropagation()
-
- if (this.state.loading) {
- return false
- }
-
- return await this.submit()
- }
- }
-
- handleOnMentionSearch = lodash.debounce(async (value) => {
- const results = await SearchModel.userSearch(`username:${value}`)
-
- this.setState({
- 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)
-
- // update the file
- updatedFileList[index] = newValue
-
- // update the state
- this.setState({
- fileList: updatedFileList
- })
-
- return updatedFileList
- }
-
- handleManualUpload = async (file) => {
- if (!file) {
- throw new Error(`No file provided`)
- }
-
- const isValidFormat = (fileType) => {
- return this.state.postingPolicy.acceptedMimeTypes.includes(fileType)
- }
-
- if (!isValidFormat(file.type)) {
- app.cores.notifications.new({
- type: "error",
- title: `Invalid format (${file.type})`,
- message: "Only the following file formats are allowed: " + this.state.postingPolicy.acceptedMimeTypes.join(", ")
- })
- return null
- throw new Error(`Invalid file format`)
- }
-
- file.thumbUrl = URL.createObjectURL(file)
- file.uid = `${file.name}-${Math.random() * 1000}`
-
- file.status = "uploading"
-
- // add file to the uploader
- this.onUploaderChange({
- file,
- fileList: [...this.state.fileList, file],
- })
-
- // upload the file
- await this.uploadFile({
- file,
- onSuccess: (response) => {
- file.status = "done"
- file.response = response
-
- this.onUploaderChange({
- file: file,
- fileList: this.updateFileList(file.uid, file)
- })
- },
- onError: (error) => {
- file.status = "error"
- file.error = error
-
- this.onUploaderChange({
- file: file,
- fileList: this.updateFileList(file.uid, file)
- })
- }
- })
-
- return file
- }
-
- 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")
-
- if (!hasFile) {
- return false
- }
-
- for (let index = 0; index < clipboardData.items.length; index++) {
- const item = clipboardData.items[index]
-
- let file = await clipboardEventFileToFile(item).catch((error) => {
- console.error(error)
- app.message.error(`Failed to upload file:`, error.message)
-
- return false
- })
-
- this.handleManualUpload(file).catch((error) => {
- console.error(error)
- return false
- })
- }
- }
- }
-
- renderUploadPreviewItem = (item, file, list, actions) => {
- const uploading = file.status === "uploading"
-
- const onClickDelete = () => {
- this.removeAttachment(file.uid)
- }
-
- return
-
-

-
-
-
- {
- uploading &&
- }
- {
- !uploading &&
- }
- />
-
- }
-
-
- }
-
- handleDrag = (event) => {
- event.preventDefault()
- event.stopPropagation()
-
- console.log(event)
-
- if (event.type === "dragenter") {
- 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)) {
- this.toggleUploaderVisibility(false)
- }
- }
- }
-
- handleUploadClick = () => {
- // create a new dialog
- const dialog = document.createElement("input")
-
- // set the dialog type to file
- dialog.type = "file"
-
- // set the dialog accept to the accepted files
- dialog.accept = this.state.postingPolicy.acceptedMimeTypes
-
- dialog.multiple = true
-
- // add a listener to the dialog
- dialog.addEventListener("change", (event) => {
- // get the files
- const files = event.target.files
-
- // loop through the files
- for (let index = 0; index < files.length; index++) {
- const file = files[index]
-
- this.handleManualUpload(file).catch((error) => {
- console.error(error)
- return false
- })
- }
- })
-
- // click the dialog
- dialog.click()
- }
-
- handleAddPoll = () => {
- if (!this.state.postPoll) {
- this.setState({
- postPoll: []
- })
- }
- }
-
- handleDeletePoll = () => {
- this.setState({
- postPoll: null
- })
- }
-
- componentDidMount = async () => {
- if (this.props.edit_post) {
- await this.setState({
- loading: true,
- postId: this.props.edit_post,
- })
-
- const post = await PostModel.getPost({ post_id: this.props.edit_post })
-
- await this.setState({
- loading: false,
- postMessage: post.message,
- postAttachments: post.attachments.map((attachment) => {
- return {
- ...attachment,
- uid: attachment.id,
- }
- }),
- fileList: post.attachments.map((attachment) => {
- return {
- ...attachment,
- uid: attachment.id,
- id: attachment.id,
- thumbUrl: attachment.url,
- status: "done",
- }
- }),
- postPoll: post.poll_options
- })
- }
- // fetch the posting policy
- //this.fetchUploadPolicy()
-
- // add a listener to the window
- document.addEventListener("paste", this.handlePaste)
- }
-
- componentWillUnmount() {
- document.removeEventListener("paste", this.handlePaste)
- }
-
- componentDidUpdate(prevProps, prevState) {
- // if pending is not empty and is not loading
- if (this.state.pending.length > 0 && !this.state.loading) {
- this.setState({ loading: true })
- } else if (this.state.pending.length === 0 && this.state.loading) {
- this.setState({ loading: false })
- }
- }
-
- render() {
- 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
- {
- showHeader &&
- {
- this.props.edit_post &&
- }
-
- {
- this.props.reply_to &&
-
-
- Replaying to
-
-
-
{
- this.props.close()
- app.navigation.goToPost(this.props.reply_to)
- }}
-
- />
-
- }
-
- }
-
-
-
-

-
-
-
{
- return {
- key: item.id,
- value: item.username,
- label: <>
-
- {item.username}
- >,
- }
- })}
- onSearch={this.handleOnMentionSearch}
- />
-
-
-
: (editMode ?
:
)}
- />
-
-
-
-
-
-
-
Drag and drop files here
- Max {humanSize.fromBytes(postingPolicy.maximumFileSize)}
-
-
-
-
- {
- this.state.postPoll &&
- }
-
-
-
}
- />
-
-
}
- onClick={this.handleAddPoll}
- />
-
-
- }
-}
\ No newline at end of file
+ state = {
+ pending: [],
+ loading: false,
+ uploaderVisible: false,
+
+ postMessage: "",
+ postAttachments: [],
+ postPoll: null,
+ postVisibility: "public",
+
+ fileList: [],
+ postingPolicy: DEFAULT_POST_POLICY,
+
+ mentionsLoadedData: [],
+ }
+
+ pollRef = React.createRef()
+
+ creatorRef = React.createRef()
+
+ cleanPostData = () => {
+ this.setState({
+ postMessage: "",
+ postAttachments: [],
+ fileList: [],
+ })
+ }
+
+ toggleUploaderVisibility = (to) => {
+ to = to ?? !this.state.uploaderVisible
+
+ if (to === this.state.uploaderVisible) {
+ return
+ }
+
+ this.setState({
+ uploaderVisible: to,
+ })
+ }
+
+ canSubmit = () => {
+ const { postMessage, postAttachments, pending, postingPolicy } =
+ this.state
+
+ const messageLengthValid =
+ postMessage.length !== 0 &&
+ postMessage.length < postingPolicy.maxMessageLength
+
+ if (pending.length !== 0) {
+ return false
+ }
+
+ if (!messageLengthValid && postAttachments.length === 0) {
+ return false
+ }
+
+ return true
+ }
+
+ submit = lodash.debounce(async () => {
+ if (this.state.loading) {
+ return false
+ }
+
+ if (!this.canSubmit()) {
+ return false
+ }
+
+ await this.setState({
+ loading: true,
+ uploaderVisible: false,
+ })
+
+ const { postMessage, postAttachments } = this.state
+
+ const payload = {
+ message: postMessage,
+ attachments: postAttachments,
+ timestamp: DateTime.local().toISO(),
+ visibility: this.state.postVisibility,
+ }
+
+ if (this.pollRef.current) {
+ let { options } = this.pollRef.current.getFieldsValue()
+
+ payload.poll_options = options.filter((option) => !!option.label)
+ }
+
+ let response = null
+
+ if (this.props.reply_to) {
+ payload.reply_to = this.props.reply_to
+ }
+
+ if (this.props.edit_post) {
+ 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) => {
+ console.error(error)
+ antd.message.error(error)
+
+ return false
+ })
+ }
+
+ this.setState({
+ loading: false,
+ })
+
+ if (response) {
+ this.cleanPostData()
+
+ if (typeof this.props.onPost === "function") {
+ this.props.onPost()
+ }
+
+ if (typeof this.props.close === "function") {
+ this.props.close()
+ }
+
+ if (this.props.reply_to) {
+ app.navigation.goToPost(this.props.reply_to)
+ }
+ }
+ }, 50)
+
+ uploadFile = async (req) => {
+ this.toggleUploaderVisibility(false)
+
+ const request = await app.cores.remoteStorage
+ .uploadFile(req.file)
+ .catch((error) => {
+ console.error(error)
+ antd.message.error(error)
+
+ req.onError(error)
+
+ return false
+ })
+
+ if (request) {
+ console.log(`Upload done >`, request)
+
+ return req.onSuccess(request)
+ }
+ }
+
+ 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,
+ ),
+ })
+ }
+
+ addAttachment = (file) => {
+ if (Array.isArray(file)) {
+ return this.setState({
+ postAttachments: [...this.state.postAttachments, ...file],
+ })
+ }
+
+ return this.setState({
+ postAttachments: [...this.state.postAttachments, file],
+ })
+ }
+
+ uploaderScrollToEnd = () => {
+ // scroll to max right
+ const element = document.querySelector(".ant-upload-list-picture-card")
+
+ // calculate the element's width and scroll to the end
+ const scrollToLeft = element.scrollWidth - element.clientWidth
+
+ if (element) {
+ element.scrollTo({
+ top: 0,
+ left: scrollToLeft,
+ behavior: "smooth",
+ })
+ }
+ }
+
+ onUploaderChange = (change) => {
+ if (this.state.fileList !== change.fileList) {
+ this.setState({
+ fileList: change.fileList,
+ })
+ }
+
+ switch (change.file.status) {
+ case "uploading": {
+ this.toggleUploaderVisibility(false)
+
+ this.setState({
+ pending: [...this.state.pending, change.file.uid],
+ })
+
+ this.uploaderScrollToEnd()
+
+ break
+ }
+ case "done": {
+ // remove pending file
+ this.setState({
+ pending: this.state.pending.filter(
+ (uid) => uid !== change.file.uid,
+ ),
+ })
+
+ if (Array.isArray(change.file.response.files)) {
+ change.file.response.files.forEach((file) => {
+ this.addAttachment(file)
+ })
+ } else {
+ this.addAttachment(change.file.response)
+ }
+
+ // scroll to end
+ this.uploaderScrollToEnd()
+
+ break
+ }
+ case "error": {
+ // remove pending file
+ this.setState({
+ pending: this.state.pending.filter(
+ (uid) => uid !== change.file.uid,
+ ),
+ })
+
+ // remove file from list
+ this.removeAttachment(change.file.uid)
+ }
+ default: {
+ break
+ }
+ }
+ }
+
+ handleMessageInputChange = (inputText) => {
+ // if the fist character is a space or a whitespace remove it
+ if (inputText[0] === " " || inputText[0] === "\n") {
+ inputText = inputText.slice(1)
+ }
+
+ this.setState({
+ postMessage: inputText,
+ })
+ }
+
+ handleMessageInputKeydown = async (e) => {
+ // detect if the user pressed `enter` key and submit the form, but only if the `shift` key is not pressed
+ if (e.keyCode === 13 && !e.shiftKey) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ if (this.state.loading) {
+ return false
+ }
+
+ return await this.submit()
+ }
+ }
+
+ handleOnMentionSearch = lodash.debounce(async (value) => {
+ const results = await SearchModel.userSearch(`username:${value}`)
+
+ this.setState({
+ 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)
+
+ // update the file
+ updatedFileList[index] = newValue
+
+ // update the state
+ this.setState({
+ fileList: updatedFileList,
+ })
+
+ return updatedFileList
+ }
+
+ handleManualUpload = async (file) => {
+ if (!file) {
+ throw new Error(`No file provided`)
+ }
+
+ const isValidFormat = (fileType) => {
+ return this.state.postingPolicy.acceptedMimeTypes.includes(fileType)
+ }
+
+ if (!isValidFormat(file.type)) {
+ app.cores.notifications.new({
+ type: "error",
+ title: `Invalid format (${file.type})`,
+ message:
+ "Only the following file formats are allowed: " +
+ this.state.postingPolicy.acceptedMimeTypes.join(", "),
+ })
+ return null
+ throw new Error(`Invalid file format`)
+ }
+
+ file.thumbUrl = URL.createObjectURL(file)
+ file.uid = `${file.name}-${Math.random() * 1000}`
+
+ file.status = "uploading"
+
+ // add file to the uploader
+ this.onUploaderChange({
+ file,
+ fileList: [...this.state.fileList, file],
+ })
+
+ // upload the file
+ await this.uploadFile({
+ file,
+ onSuccess: (response) => {
+ file.status = "done"
+ file.response = response
+
+ this.onUploaderChange({
+ file: file,
+ fileList: this.updateFileList(file.uid, file),
+ })
+ },
+ onError: (error) => {
+ file.status = "error"
+ file.error = error
+
+ this.onUploaderChange({
+ file: file,
+ fileList: this.updateFileList(file.uid, file),
+ })
+ },
+ })
+
+ return file
+ }
+
+ 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",
+ )
+
+ if (!hasFile) {
+ return false
+ }
+
+ for (let index = 0; index < clipboardData.items.length; index++) {
+ const item = clipboardData.items[index]
+
+ let file = await clipboardEventFileToFile(item).catch(
+ (error) => {
+ console.error(error)
+ app.message.error(
+ `Failed to upload file:`,
+ error.message,
+ )
+
+ return false
+ },
+ )
+
+ this.handleManualUpload(file).catch((error) => {
+ console.error(error)
+ return false
+ })
+ }
+ }
+ }
+
+ handleVisibilityChange = (key) => {
+ this.setState({ postVisibility: key })
+ }
+
+ renderUploadPreviewItem = (item, file, list, actions) => {
+ const uploading = file.status === "uploading"
+
+ const onClickDelete = () => {
+ this.removeAttachment(file.uid)
+ }
+
+ return (
+
+
+

+
+
+
+ {uploading && (
+
+ )}
+ {!uploading && (
+
+ } />
+
+ )}
+
+
+ )
+ }
+
+ handleDrag = (event) => {
+ event.preventDefault()
+ event.stopPropagation()
+
+ console.log(event)
+
+ if (event.type === "dragenter") {
+ 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)
+ ) {
+ this.toggleUploaderVisibility(false)
+ }
+ }
+ }
+
+ handleUploadClick = () => {
+ // create a new dialog
+ const dialog = document.createElement("input")
+
+ // set the dialog type to file
+ dialog.type = "file"
+
+ // set the dialog accept to the accepted files
+ dialog.accept = this.state.postingPolicy.acceptedMimeTypes
+
+ dialog.multiple = true
+
+ // add a listener to the dialog
+ dialog.addEventListener("change", (event) => {
+ // get the files
+ const files = event.target.files
+
+ // loop through the files
+ for (let index = 0; index < files.length; index++) {
+ const file = files[index]
+
+ this.handleManualUpload(file).catch((error) => {
+ console.error(error)
+ return false
+ })
+ }
+ })
+
+ // click the dialog
+ dialog.click()
+ }
+
+ handleAddPoll = () => {
+ if (!this.state.postPoll) {
+ this.setState({
+ postPoll: [],
+ })
+ }
+ }
+
+ handleDeletePoll = () => {
+ this.setState({
+ postPoll: null,
+ })
+ }
+
+ componentDidMount = async () => {
+ if (this.props.edit_post) {
+ await this.setState({
+ loading: true,
+ postId: this.props.edit_post,
+ })
+
+ const post = await PostModel.getPost({
+ post_id: this.props.edit_post,
+ })
+
+ await this.setState({
+ loading: false,
+ postMessage: post.message,
+ postAttachments: post.attachments.map((attachment) => {
+ return {
+ ...attachment,
+ uid: attachment.id,
+ }
+ }),
+ fileList: post.attachments.map((attachment) => {
+ return {
+ ...attachment,
+ uid: attachment.id,
+ id: attachment.id,
+ thumbUrl: attachment.url,
+ status: "done",
+ }
+ }),
+ postPoll: post.poll_options,
+ postVisibility: post.visibility,
+ })
+ }
+ // fetch the posting policy
+ //this.fetchUploadPolicy()
+
+ // add a listener to the window
+ document.addEventListener("paste", this.handlePaste)
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("paste", this.handlePaste)
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ // if pending is not empty and is not loading
+ if (this.state.pending.length > 0 && !this.state.loading) {
+ this.setState({ loading: true })
+ } else if (this.state.pending.length === 0 && this.state.loading) {
+ this.setState({ loading: false })
+ }
+ }
+
+ render() {
+ 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 (
+
+ {showHeader && (
+
+ {this.props.edit_post && (
+
+ )}
+
+ {this.props.reply_to && (
+
+
+
+ Replaying to
+
+
+
{
+ this.props.close()
+ app.navigation.goToPost(
+ this.props.reply_to,
+ )
+ }}
+ />
+
+ )}
+
+ )}
+
+
+
+

+
+
+
{
+ return {
+ key: item.id,
+ value: item.username,
+ label: (
+ <>
+
+ {item.username}
+ >
+ ),
+ }
+ })}
+ onSearch={this.handleOnMentionSearch}
+ />
+
+
+
+ ) : editMode ? (
+
+ ) : (
+
+ )
+ }
+ />
+
+
+
+
+
+
+
Drag and drop files here
+
+ Max{" "}
+ {humanSize.fromBytes(
+ postingPolicy.maximumFileSize,
+ )}
+
+
+
+
+
+ {this.state.postPoll && (
+
+ )}
+
+
+
}
+ />
+
+
}
+ onClick={this.handleAddPoll}
+ />
+
+
+
+
+ )
+ }
+}
diff --git a/packages/app/src/components/PostCreator/index.less b/packages/app/src/components/PostCreator/index.less
index 4564e970..37fc22f3 100755
--- a/packages/app/src/components/PostCreator/index.less
+++ b/packages/app/src/components/PostCreator/index.less
@@ -1,380 +1,387 @@
@file_preview_borderRadius: 7px;
.postCreator {
- position: relative;
+ position: relative;
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
- align-items: center;
+ align-items: center;
- width: 100%;
- z-index: 55;
+ width: 100%;
+ z-index: 55;
- max-width: 600px;
- border-radius: 12px;
+ max-width: 600px;
+ border-radius: 12px;
- background-color: var(--background-color-accent);
- padding: 15px;
+ background-color: var(--background-color-accent);
+ padding: 15px;
- gap: 10px;
+ gap: 10px;
- .postCreator-header {
- display: flex;
- flex-direction: row;
+ .postCreator-header {
+ display: flex;
+ flex-direction: row;
- align-items: center;
+ align-items: center;
- width: 100%;
+ width: 100%;
- p {
- margin: 0;
- }
+ p {
+ margin: 0;
+ }
- .postCreator-header-indicator {
- display: flex;
- flex-direction: row;
+ .postCreator-header-indicator {
+ display: flex;
+ flex-direction: row;
- gap: 7px;
+ gap: 7px;
- align-items: center;
- }
- }
+ align-items: center;
+ }
+ }
- .actions {
- display: inline-flex;
- flex-direction: row;
+ .actions {
+ display: inline-flex;
+ flex-direction: row;
- align-items: center;
- justify-content: flex-start;
+ align-items: center;
+ justify-content: flex-start;
- width: 100%;
- padding: 0 10px;
+ width: 100%;
+ padding: 0 10px;
- gap: 10px;
+ gap: 10px;
- transition: all 150ms ease-in-out;
+ transition: all 150ms ease-in-out;
- .ant-btn {
- font-size: 1.2rem;
+ .ant-btn {
+ font-size: 1.2rem;
- color: var(--text-color);
+ color: var(--text-color);
- opacity: 0.7;
+ opacity: 0.7;
- svg {
- margin: 0;
- }
- }
- }
+ svg {
+ margin: 0;
+ }
+ }
+ }
- .uploader {
- position: relative;
+ .uploader {
+ position: relative;
- display: flex;
- flex-direction: row;
+ display: flex;
+ flex-direction: row;
- align-items: center;
- justify-content: center;
+ align-items: center;
+ justify-content: center;
- width: 100%;
- height: 100%;
+ width: 100%;
+ height: 100%;
- overflow: hidden;
- overflow-x: scroll;
+ overflow: hidden;
+ overflow-x: scroll;
- transition: all 150ms ease-in-out;
+ transition: all 150ms ease-in-out;
- &.visible {
- position: absolute;
- top: 0;
- left: 0;
+ &.visible {
+ position: absolute;
+ top: 0;
+ left: 0;
- width: 100%;
- height: 100%;
+ width: 100%;
+ height: 100%;
- z-index: 150;
+ z-index: 150;
- display: flex;
+ display: flex;
- padding: 0;
+ padding: 0;
- align-items: center;
- justify-content: center;
+ align-items: center;
+ justify-content: center;
- border: 0;
+ border: 0;
- .ant-upload-drag {
- width: 100%;
- height: 100%;
+ .ant-upload-drag {
+ width: 100%;
+ height: 100%;
- opacity: 1;
- }
+ opacity: 1;
+ }
- .ant-upload-list {
- opacity: 0;
- }
- }
+ .ant-upload-list {
+ opacity: 0;
+ }
+ }
- .ant-upload-drag {
- opacity: 0;
- width: 0;
- height: 0;
+ .ant-upload-drag {
+ opacity: 0;
+ width: 0;
+ height: 0;
- background-color: transparent !important;
- border: 0;
- }
+ background-color: transparent !important;
+ border: 0;
+ }
- .ant-upload-wrapper {
- position: relative;
+ .ant-upload-wrapper {
+ position: relative;
- display: flex;
- flex-direction: column;
+ display: flex;
+ flex-direction: column;
- justify-content: center;
+ justify-content: center;
- background-color: transparent;
- backdrop-filter: blur(8px);
+ background-color: transparent;
+ backdrop-filter: blur(8px);
- width: 100%;
- height: 100%;
+ width: 100%;
+ height: 100%;
- overflow: hidden;
+ overflow: hidden;
- .ant-upload-list {
- display: flex;
- flex-direction: row;
+ .ant-upload-list {
+ display: flex;
+ flex-direction: row;
- align-items: center;
+ align-items: center;
- width: 100%;
+ width: 100%;
- overflow-x: auto;
- overflow-y: hidden;
+ overflow-x: auto;
+ overflow-y: hidden;
- white-space: nowrap;
+ white-space: nowrap;
- align-items: center;
- justify-content: flex-start;
+ align-items: center;
+ justify-content: flex-start;
- gap: 10px;
+ gap: 10px;
- border-radius: @file_preview_borderRadius;
+ border-radius: @file_preview_borderRadius;
- .ant-upload-list-item-container {
- background-color: rgba(var(--bg_color_5), 1);
+ .ant-upload-list-item-container {
+ background-color: rgba(var(--bg_color_5), 1);
- width: fit-content;
- height: fit-content;
+ width: fit-content;
+ height: fit-content;
- border-radius: @file_preview_borderRadius;
+ border-radius: @file_preview_borderRadius;
- margin: 0;
- }
- }
- }
+ margin: 0;
+ }
+ }
+ }
- .ant-upload-list-item-actions {
- display: flex;
- align-items: center;
- justify-content: center;
+ .ant-upload-list-item-actions {
+ display: flex;
+ align-items: center;
+ justify-content: center;
- height: 100%;
- width: 100%;
+ height: 100%;
+ width: 100%;
- font-size: 2rem;
+ font-size: 2rem;
- border-radius: @file_preview_borderRadius;
- }
+ border-radius: @file_preview_borderRadius;
+ }
- .hint {
- display: flex;
- flex-direction: column;
+ .hint {
+ display: flex;
+ flex-direction: column;
- align-items: center;
- justify-content: center;
- align-self: center;
+ align-items: center;
+ justify-content: center;
+ align-self: center;
- text-align: center;
+ text-align: center;
- background-color: transparent;
+ background-color: transparent;
- font-size: 0.8rem;
+ font-size: 0.8rem;
- padding: 10px;
+ padding: 10px;
- svg {
- font-size: 1.5rem;
- margin-bottom: 6px;
- margin-right: 0 !important;
- }
- }
+ svg {
+ font-size: 1.5rem;
+ margin-bottom: 6px;
+ margin-right: 0 !important;
+ }
+ }
- .file {
- position: relative;
+ .file {
+ position: relative;
- width: 10vw;
- height: 10vw;
+ width: 10vw;
+ height: 10vw;
- max-width: 150px;
- max-height: 150px;
+ max-width: 150px;
+ max-height: 150px;
- font-size: 2rem;
+ font-size: 2rem;
- transition: all 150ms ease-in-out;
+ transition: all 150ms ease-in-out;
- border-radius: 10px;
+ border-radius: 10px;
- &.uploading {
- img {
- opacity: 0.5;
- }
+ &.uploading {
+ img {
+ opacity: 0.5;
+ }
- .actions {
- backdrop-filter: blur(5px);
- opacity: 1;
- }
- }
+ .actions {
+ backdrop-filter: blur(5px);
+ opacity: 1;
+ }
+ }
- &:hover {
- .actions {
- opacity: 1;
- backdrop-filter: blur(2px);
- }
+ &:hover {
+ .actions {
+ opacity: 1;
+ backdrop-filter: blur(2px);
+ }
- .preview {
- opacity: 0.5;
- }
- }
+ .preview {
+ opacity: 0.5;
+ }
+ }
- .preview {
- position: absolute;
+ .preview {
+ position: absolute;
- top: 0;
- left: 0;
+ top: 0;
+ left: 0;
- width: 100%;
- height: 100%;
+ width: 100%;
+ height: 100%;
- img {
- width: 100%;
- height: 100%;
+ img {
+ width: 100%;
+ height: 100%;
- transition: all 150ms ease-in-out;
- border-radius: @file_preview_borderRadius;
+ transition: all 150ms ease-in-out;
+ border-radius: @file_preview_borderRadius;
- object-fit: contain;
- }
- }
+ object-fit: contain;
+ }
+ }
+ }
+ }
+
+ .actions {
+ display: flex;
+ flex-direction: inline-flex;
+
+ svg {
+ color: var(--text-color);
+ margin: 0;
+ }
- .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;
-
- svg {
- color: rgba(var(--bg_color_1), 1);
- margin: 0;
- }
- }
- }
- }
-
- .textInput {
- display: flex;
-
- width: 100%;
-
- transition: all 150ms ease-in-out;
-
- background-color: transparent;
-
- svg {
- margin: 0 !important;
- }
-
- .avatar {
- width: fit-content;
- height: 45px;
-
- display: flex;
-
- img {
- width: 45px;
- height: 45px;
- border-radius: 12px;
- }
- }
-
- textarea {
- color: var(--text-color);
- }
-
- textarea::placeholder {
- color: rgb(var(--bg_color_4));
- }
-
- .textArea {
- border-radius: 8px !important;
- transition: all 150ms ease-in-out !important;
-
- &.active {
- background-color: var(--background-color-primary);
- }
- }
-
- .ant-btn-primary {
- z-index: 10;
- position: relative;
- border-radius: 0 10px 10px 0;
- height: 100%;
- vertical-align: bottom;
- border: none;
- box-shadow: none;
-
- svg {
- color: var(--text-color) !important;
- }
- }
-
- .ant-input {
- background-color: transparent;
-
- z-index: 10;
- position: relative;
- border-color: transparent !important;
- box-shadow: none;
- border-radius: 3px 0 0;
- height: 100%;
- padding: 5px 10px;
- transition: height 150ms linear;
- width: 100%;
- }
-
- .ant-btn-primary[disabled] {
- background-color: var(--background-color-accent);
- }
-
- .ant-input-affix-wrapper {
- height: 100%;
-
- border: 0;
- outline: 0;
- box-shadow: none;
-
- background-color: transparent;
- }
- }
-}
\ No newline at end of file
+ .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;
+ }
+ }
+ }
+
+ .textInput {
+ display: flex;
+
+ width: 100%;
+
+ transition: all 150ms ease-in-out;
+
+ background-color: transparent;
+
+ svg {
+ margin: 0 !important;
+ }
+
+ .avatar {
+ width: fit-content;
+ height: 45px;
+
+ display: flex;
+
+ img {
+ width: 45px;
+ height: 45px;
+ border-radius: 12px;
+ }
+ }
+
+ textarea {
+ color: var(--text-color);
+ }
+
+ textarea::placeholder {
+ color: rgb(var(--bg_color_4));
+ }
+
+ .textArea {
+ border-radius: 8px !important;
+ transition: all 150ms ease-in-out !important;
+
+ &.active {
+ background-color: var(--background-color-primary);
+ }
+ }
+
+ .ant-btn-primary {
+ z-index: 10;
+ position: relative;
+ border-radius: 0 10px 10px 0;
+ height: 100%;
+ vertical-align: bottom;
+ border: none;
+ box-shadow: none;
+
+ svg {
+ color: var(--text-color) !important;
+ }
+ }
+
+ .ant-input {
+ background-color: transparent;
+
+ z-index: 10;
+ position: relative;
+ border-color: transparent !important;
+ box-shadow: none;
+ border-radius: 3px 0 0;
+ height: 100%;
+ padding: 5px 10px;
+ transition: height 150ms linear;
+ width: 100%;
+ }
+
+ .ant-btn-primary[disabled] {
+ background-color: var(--background-color-accent);
+ }
+
+ .ant-input-affix-wrapper {
+ height: 100%;
+
+ border: 0;
+ outline: 0;
+ box-shadow: none;
+
+ background-color: transparent;
+ }
+ }
+}
diff --git a/packages/app/src/components/PostsList/index.jsx b/packages/app/src/components/PostsList/index.jsx
index 4a651d36..4a1d6ed6 100755
--- a/packages/app/src/components/PostsList/index.jsx
+++ b/packages/app/src/components/PostsList/index.jsx
@@ -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")
}
}
diff --git a/packages/server/db_models/post/index.js b/packages/server/db_models/post/index.js
index a8b402d9..5fdc1844 100755
--- a/packages/server/db_models/post/index.js
+++ b/packages/server/db_models/post/index.js
@@ -1,37 +1,41 @@
export default {
- name: "Post",
- collection: "posts",
- schema: {
- user_id: {
- type: String,
- required: true
- },
- created_at: {
- type: Date,
- required: true
- },
- message: {
- type: String
- },
- attachments: {
- type: Array,
- default: []
- },
- flags: {
- type: Array,
- default: []
- },
- reply_to: {
- type: String,
- default: null
- },
- updated_at: {
- type: String,
- default: null
- },
- poll_options: {
- type: Array,
- default: null
- }
- }
-}
\ No newline at end of file
+ name: "Post",
+ collection: "posts",
+ schema: {
+ user_id: {
+ type: String,
+ required: true,
+ },
+ created_at: {
+ type: Date,
+ required: true,
+ },
+ message: {
+ type: String,
+ },
+ attachments: {
+ type: Array,
+ default: [],
+ },
+ flags: {
+ type: Array,
+ default: [],
+ },
+ reply_to: {
+ type: String,
+ default: null,
+ },
+ updated_at: {
+ type: String,
+ default: null,
+ },
+ poll_options: {
+ type: Array,
+ default: null,
+ },
+ visibility: {
+ type: String,
+ default: "public",
+ },
+ },
+}
diff --git a/packages/server/services/posts/classes/posts/index.js b/packages/server/services/posts/classes/posts/index.js
index 1acfa5a9..7ac9aff5 100644
--- a/packages/server/services/posts/classes/posts/index.js
+++ b/packages/server/services/posts/classes/posts/index.js
@@ -1,19 +1,19 @@
export default class Posts {
- static timeline = require("./methods/timeline").default
- static globalTimeline = require("./methods/globalTimeline").default
- static data = require("./methods/data").default
- static getLiked = require("./methods/getLiked").default
- 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 toggleSave = require("./methods/toggleSave").default
- static toggleLike = require("./methods/toggleLike").default
- static report = require("./methods/report").default
- static flag = require("./methods/flag").default
- static delete = require("./methods/delete").default
- static update = require("./methods/update").default
- static replies = require("./methods/replies").default
- static votePoll = require("./methods/votePoll").default
- static deleteVotePoll = require("./methods/deletePollVote").default
-}
\ No newline at end of file
+ static timeline = require("./methods/timeline").default
+ static globalTimeline = require("./methods/globalTimeline").default
+ static data = require("./methods/data").default
+ static getLiked = require("./methods/getLiked").default
+ static getSaved = require("./methods/getSaved").default
+ static fromUserId = require("./methods/fromUserId").default
+ static create = require("./methods/create").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
+ static flag = require("./methods/flag").default
+ static delete = require("./methods/delete").default
+ static update = require("./methods/update").default
+ static replies = require("./methods/replies").default
+ static votePoll = require("./methods/votePoll").default
+ static deleteVotePoll = require("./methods/deletePollVote").default
+}
diff --git a/packages/server/services/posts/classes/posts/methods/create.js b/packages/server/services/posts/classes/posts/methods/create.js
index 6b6a5214..7083634f 100644
--- a/packages/server/services/posts/classes/posts/methods/create.js
+++ b/packages/server/services/posts/classes/posts/methods/create.js
@@ -2,73 +2,113 @@ 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 = {}) => {
- await requiredFields(["user_id"], payload)
+const visibilityOptions = ["public", "private", "only_mutuals"]
- let { user_id, message, attachments, timestamp, reply_to, poll_options } = payload
+export default async (payload = {}, req) => {
+ await requiredFields(["user_id"], payload)
- // check if is a Array and have at least one element
- const isAttachmentArray = Array.isArray(attachments) && attachments.length > 0
+ let {
+ user_id,
+ message,
+ attachments,
+ timestamp,
+ reply_to,
+ poll_options,
+ visibility = "public",
+ } = payload
- if (!isAttachmentArray && !message) {
- throw new OperationError(400, "Cannot create a post without message or attachments")
- }
+ // check if visibility is valid
+ if (!visibilityOptions.includes(visibility)) {
+ throw new OperationError(400, "Invalid visibility option")
+ }
- if (isAttachmentArray) {
- // clean empty attachments
- attachments = attachments.filter((attachment) => attachment)
+ // check if is a Array and have at least one element
+ const isAttachmentArray =
+ Array.isArray(attachments) && attachments.length > 0
- // fix attachments with url strings if needed
- attachments = attachments.map((attachment) => {
- if (typeof attachment === "string") {
- attachment = {
- url: attachment,
- }
- }
+ if (!isAttachmentArray && !message) {
+ throw new OperationError(
+ 400,
+ "Cannot create a post without message or attachments",
+ )
+ }
- return attachment
- })
- }
+ if (isAttachmentArray) {
+ // clean empty attachments
+ attachments = attachments.filter((attachment) => attachment)
- if (!timestamp) {
- timestamp = DateTime.local().toISO()
- } else {
- timestamp = DateTime.fromISO(timestamp).toISO()
- }
+ // fix attachments with url strings if needed
+ attachments = attachments.map((attachment) => {
+ if (typeof attachment === "string") {
+ attachment = {
+ url: attachment,
+ }
+ }
- if (Array.isArray(poll_options)) {
- poll_options = poll_options.map((option) => {
- if (!option.id) {
- option.id = nanoid()
- }
+ return attachment
+ })
+ }
- return option
- })
- }
+ if (!timestamp) {
+ timestamp = DateTime.local().toISO()
+ } else {
+ timestamp = DateTime.fromISO(timestamp).toISO()
+ }
- let post = new Post({
- created_at: timestamp,
- user_id: typeof user_id === "object" ? user_id.toString() : user_id,
- message: message,
- attachments: attachments ?? [],
- reply_to: reply_to,
- flags: [],
- poll_options: poll_options,
- })
+ if (Array.isArray(poll_options)) {
+ poll_options = poll_options.map((option) => {
+ if (!option.id) {
+ option.id = nanoid()
+ }
- await post.save()
+ return option
+ })
+ }
- post = post.toObject()
+ let post = new Post({
+ created_at: timestamp,
+ user_id: typeof user_id === "object" ? user_id.toString() : user_id,
+ message: message,
+ attachments: attachments ?? [],
+ reply_to: reply_to,
+ flags: [],
+ poll_options: poll_options,
+ visibility: visibility.toLocaleLowerCase(),
+ })
- const result = await fullfill({
- posts: post,
- for_user_id: user_id
- })
+ await post.save()
- // TODO: create background jobs (nsfw dectection)
- global.websocket.io.of("/").emit(`post.new`, result[0])
+ post = post.toObject()
- return post
-}
\ No newline at end of file
+ const result = await stage({
+ posts: post,
+ 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.queues.createJob("classify_post_attachments", {
+ post_id: post._id.toString(),
+ auth_token: req.headers.authorization,
+ })
+
+ return post
+}
diff --git a/packages/server/services/posts/classes/posts/methods/data.js b/packages/server/services/posts/classes/posts/methods/data.js
index c2daee3a..dbbc158b 100644
--- a/packages/server/services/posts/classes/posts/methods/data.js
+++ b/packages/server/services/posts/classes/posts/methods/data.js
@@ -1,54 +1,54 @@
import { Post } from "@db_models"
-import fullfillPostsData from "./fullfill"
+import stage from "./stage"
const maxLimit = 300
export default async (payload = {}) => {
- let {
- for_user_id,
- post_id,
- query = {},
- trim = 0,
- limit = 20,
- sort = { created_at: -1 },
- } = payload
+ let {
+ for_user_id,
+ post_id,
+ query = {},
+ trim = 0,
+ limit = 20,
+ sort = { created_at: -1 },
+ } = payload
- // set a hard limit on the number of posts to retrieve, used for pagination
- if (limit > maxLimit) {
- limit = maxLimit
- }
+ // set a hard limit on the number of posts to retrieve, used for pagination
+ if (limit > maxLimit) {
+ limit = maxLimit
+ }
- let posts = []
+ let posts = []
- if (post_id) {
- try {
- const post = await Post.findById(post_id)
+ if (post_id) {
+ try {
+ const post = await Post.findById(post_id)
- posts = [post]
- } catch (error) {
- throw new OperationError(404, "Post not found")
- }
- } else {
- posts = await Post.find({ ...query })
- .sort(sort)
- .skip(trim)
- .limit(limit)
- }
+ posts = [post]
+ } catch (error) {
+ throw new OperationError(404, "Post not found")
+ }
+ } else {
+ posts = await Post.find({ ...query })
+ .sort(sort)
+ .skip(trim)
+ .limit(limit)
+ }
- // fullfill data
- posts = await fullfillPostsData({
- posts,
- for_user_id,
- })
+ // fullfill data
+ posts = await stage({
+ posts,
+ for_user_id,
+ })
- // if post_id is specified, return only one post
- if (post_id) {
- if (posts.length === 0) {
- throw new OperationError(404, "Post not found")
- }
+ // if post_id is specified, return only one post
+ if (post_id) {
+ if (posts.length === 0) {
+ throw new OperationError(404, "Post not found")
+ }
- return posts[0]
- }
+ return posts[0]
+ }
- return posts
-}
\ No newline at end of file
+ return posts
+}
diff --git a/packages/server/services/posts/classes/posts/methods/delete.js b/packages/server/services/posts/classes/posts/methods/delete.js
index b4716971..c64a9920 100644
--- a/packages/server/services/posts/classes/posts/methods/delete.js
+++ b/packages/server/services/posts/classes/posts/methods/delete.js
@@ -1,45 +1,65 @@
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")
- }
+ if (!post_id) {
+ throw new OperationError(400, "Missing post_id")
+ }
- await Post.deleteOne({
- _id: post_id,
- }).catch((err) => {
- throw new OperationError(500, `An error has occurred: ${err.message}`)
- })
+ const post = await Post.findById(post_id)
- // search for likes
- await PostLike.deleteMany({
- post_id: post_id,
- }).catch((err) => {
- throw new OperationError(500, `An error has occurred: ${err.message}`)
- })
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
- // deleted from saved
- await PostSave.deleteMany({
- post_id: post_id,
- }).catch((err) => {
- throw new OperationError(500, `An error has occurred: ${err.message}`)
- })
+ await Post.deleteOne({
+ _id: post_id,
+ }).catch((err) => {
+ throw new OperationError(500, `An error has occurred: ${err.message}`)
+ })
- // delete replies
- await Post.deleteMany({
- reply_to: post_id,
- }).catch((err) => {
- throw new OperationError(500, `An error has occurred: ${err.message}`)
- })
+ // search for likes
+ await PostLike.deleteMany({
+ post_id: post_id,
+ }).catch((err) => {
+ 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)
+ // deleted from saved
+ await PostSave.deleteMany({
+ post_id: post_id,
+ }).catch((err) => {
+ throw new OperationError(500, `An error has occurred: ${err.message}`)
+ })
- return {
- deleted: true,
- }
-}
\ No newline at end of file
+ // delete replies
+ await Post.deleteMany({
+ reply_to: post_id,
+ }).catch((err) => {
+ throw new OperationError(500, `An error has occurred: ${err.message}`)
+ })
+
+ 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,
+ }
+}
diff --git a/packages/server/services/posts/classes/posts/methods/fullfill.js b/packages/server/services/posts/classes/posts/methods/fullfill.js
deleted file mode 100644
index 92f37000..00000000
--- a/packages/server/services/posts/classes/posts/methods/fullfill.js
+++ /dev/null
@@ -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
-}
\ No newline at end of file
diff --git a/packages/server/services/posts/classes/posts/methods/replies.js b/packages/server/services/posts/classes/posts/methods/replies.js
index 2cc8ec33..d245d2a7 100644
--- a/packages/server/services/posts/classes/posts/methods/replies.js
+++ b/packages/server/services/posts/classes/posts/methods/replies.js
@@ -1,29 +1,24 @@
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")
- }
+ if (!post_id) {
+ throw new OperationError(400, "Post ID is required")
+ }
- let posts = await Post.find({
- reply_to: post_id,
- })
- .limit(limit)
- .skip(trim)
- .sort({ created_at: -1 })
+ let posts = await Post.find({
+ reply_to: post_id,
+ })
+ .limit(limit)
+ .skip(trim)
+ .sort({ created_at: -1 })
- posts = await fullfillPostsData({
- posts,
- for_user_id,
- })
+ posts = await stage({
+ posts,
+ for_user_id,
+ })
- return posts
-}
\ No newline at end of file
+ return posts
+}
diff --git a/packages/server/services/posts/classes/posts/methods/stage.js b/packages/server/services/posts/classes/posts/methods/stage.js
new file mode 100644
index 00000000..75325b13
--- /dev/null
+++ b/packages/server/services/posts/classes/posts/methods/stage.js
@@ -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
+}
diff --git a/packages/server/services/posts/classes/posts/methods/update.js b/packages/server/services/posts/classes/posts/methods/update.js
index f80b1fac..9db0334d 100644
--- a/packages/server/services/posts/classes/posts/methods/update.js
+++ b/packages/server/services/posts/classes/posts/methods/update.js
@@ -1,42 +1,60 @@
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)
+ let post = await Post.findById(post_id)
- if (!post) {
- throw new OperationError(404, "Post not found")
- }
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
- const updateKeys = Object.keys(update)
+ const updateKeys = Object.keys(update)
- updateKeys.forEach((key) => {
- post[key] = update[key]
- })
+ updateKeys.forEach((key) => {
+ post[key] = update[key]
+ })
- post.updated_at = DateTime.local().toISO()
+ post.updated_at = DateTime.local().toISO()
- if (Array.isArray(update.poll_options)) {
- post.poll_options = update.poll_options.map((option) => {
- if (!option.id) {
- option.id = nanoid()
- }
+ if (Array.isArray(update.poll_options)) {
+ post.poll_options = update.poll_options.map((option) => {
+ if (!option.id) {
+ option.id = nanoid()
+ }
- return option
- })
- }
+ return option
+ })
+ }
- await post.save()
+ await post.save()
- post = post.toObject()
+ post = post.toObject()
- const result = await fullfill({
- posts: post,
- })
+ 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])
+ }
- return result[0]
-}
\ No newline at end of file
+ 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]
+}
diff --git a/packages/server/services/posts/posts.service.js b/packages/server/services/posts/posts.service.js
index 9291e4e4..7ddda673 100644
--- a/packages/server/services/posts/posts.service.js
+++ b/packages/server/services/posts/posts.service.js
@@ -2,30 +2,72 @@ 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"
-export default class API extends Server {
- static refName = "posts"
- static enableWebsockets = true
- static routesPath = `${__dirname}/routes`
- static listen_port = process.env.HTTP_LISTEN_PORT ?? 3001
+// wsfast
+import HyperExpress from "hyper-express"
- middlewares = {
- ...SharedMiddlewares
- }
+class WSFastServer {
+ router = new HyperExpress.Router()
- contexts = {
- db: new DbManager(),
- redis: RedisClient(),
- }
+ clients = new Set()
- async onInitialize() {
- await this.contexts.db.initialize()
- await this.contexts.redis.initialize()
- }
+ routes = {
+ connect: async (socket) => {
+ console.log("Client connected", socket)
+ },
+ }
- handleWsAuth = require("@shared-lib/handleWsAuth").default
+ 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)
+ }
}
-Boot(API)
\ No newline at end of file
+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,
+ }
+
+ 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
+}
+
+Boot(API)
diff --git a/packages/server/services/posts/queues/classify_post_attachments/index.js b/packages/server/services/posts/queues/classify_post_attachments/index.js
new file mode 100644
index 00000000..c5609f0c
--- /dev/null
+++ b/packages/server/services/posts/queues/classify_post_attachments/index.js
@@ -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)
+ },
+}
diff --git a/packages/server/services/posts/routes/posts/[post_id]/update/put.js b/packages/server/services/posts/routes/posts/[post_id]/update/put.js
index d48c340d..1cdd9439 100644
--- a/packages/server/services/posts/routes/posts/[post_id]/update/put.js
+++ b/packages/server/services/posts/routes/posts/[post_id]/update/put.js
@@ -1,44 +1,56 @@
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 {
- middlewares: ["withAuthentication"],
- fn: async (req) => {
- let update = {}
+ middlewares: ["withAuthentication"],
+ fn: async (req) => {
+ let update = {}
- const post = await Post.findById(req.params.post_id)
+ const post = await Post.findById(req.params.post_id)
- if (!post) {
- throw new OperationError(404, "Post not found")
- }
+ if (!post) {
+ throw new OperationError(404, "Post not found")
+ }
- if (post.user_id !== req.auth.session.user_id) {
- throw new OperationError(403, "You cannot edit this post")
- }
+ if (post.user_id !== req.auth.session.user_id) {
+ throw new OperationError(403, "You cannot edit this post")
+ }
- AllowedFields.forEach((key) => {
- if (typeof req.body[key] !== "undefined") {
- // check maximung strings length
- 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])
- } else {
- update[key] = req.body[key]
- }
- } else {
- update[key] = req.body[key]
- }
- }
- })
+ AllowedFields.forEach((key) => {
+ if (typeof req.body[key] !== "undefined") {
+ // check maximung strings length
+ 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],
+ )
+ } else {
+ update[key] = req.body[key]
+ }
+ } else {
+ update[key] = req.body[key]
+ }
+ }
+ })
- return await PostClass.update(req.params.post_id, update)
- }
-}
\ No newline at end of file
+ return await PostClass.update(req.params.post_id, update)
+ },
+}
diff --git a/packages/server/services/posts/routes/posts/new/post.js b/packages/server/services/posts/routes/posts/new/post.js
index e6f857c2..24739265 100644
--- a/packages/server/services/posts/routes/posts/new/post.js
+++ b/packages/server/services/posts/routes/posts/new/post.js
@@ -1,13 +1,16 @@
import Posts from "@classes/posts"
export default {
- middlewares: ["withAuthentication"],
- fn: async (req, res) => {
- const result = await Posts.create({
- ...req.body,
- user_id: req.auth.session.user_id,
- })
+ middlewares: ["withAuthentication"],
+ fn: async (req, res) => {
+ const result = await Posts.create(
+ {
+ ...req.body,
+ user_id: req.auth.session.user_id,
+ },
+ req,
+ )
- return result
- }
-}
\ No newline at end of file
+ return result
+ },
+}
diff --git a/packages/server/services/posts/routes/posts/trending/[trending]/get.js b/packages/server/services/posts/routes/posts/trending/[trending]/get.js
index 1a022c49..753b9302 100644
--- a/packages/server/services/posts/routes/posts/trending/[trending]/get.js
+++ b/packages/server/services/posts/routes/posts/trending/[trending]/get.js
@@ -1,25 +1,25 @@
import { Post } from "@db_models"
-import fullfill from "@classes/posts/methods/fullfill"
+import stage from "@classes/posts/methods/stage"
export default {
- middlewares: ["withOptionalAuthentication"],
- fn: async (req) => {
- const { limit, trim } = req.query
+ middlewares: ["withOptionalAuthentication"],
+ fn: async (req) => {
+ const { limit, trim } = req.query
- let result = await Post.find({
- message: {
- $regex: new RegExp(`#${req.params.trending}`, "gi")
- }
- })
- .sort({ created_at: -1 })
- .skip(trim ?? 0)
- .limit(limit ?? 20)
+ let result = await Post.find({
+ message: {
+ $regex: new RegExp(`#${req.params.trending}`, "gi"),
+ },
+ })
+ .sort({ created_at: -1 })
+ .skip(trim ?? 0)
+ .limit(limit ?? 20)
- result = await fullfill({
- posts: result,
- for_user_id: req.auth.session.user_id,
- })
+ result = await stage({
+ posts: result,
+ for_user_id: req.auth.session.user_id,
+ })
- return result
- }
-}
\ No newline at end of file
+ return result
+ },
+}
diff --git a/packages/server/services/posts/routes_ws/connect_realtime.js b/packages/server/services/posts/routes_ws/connect_realtime.js
new file mode 100644
index 00000000..f06abf0a
--- /dev/null
+++ b/packages/server/services/posts/routes_ws/connect_realtime.js
@@ -0,0 +1,4 @@
+export default async function (socket) {
+ console.log(`Socket ${socket.id} connected to realtime posts`)
+ socket.join("global:posts:realtime")
+}
diff --git a/packages/server/services/posts/routes_ws/disconnect_realtime.js b/packages/server/services/posts/routes_ws/disconnect_realtime.js
new file mode 100644
index 00000000..90372242
--- /dev/null
+++ b/packages/server/services/posts/routes_ws/disconnect_realtime.js
@@ -0,0 +1,4 @@
+export default async function (socket) {
+ console.log(`Socket ${socket.id} disconnected from realtime posts`)
+ socket.leave("global:posts:realtime")
+}