mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-09 10:34:17 +00:00
merge from local
This commit is contained in:
parent
75f1dddf48
commit
dfcafc6b18
@ -336,6 +336,10 @@ class ComtyApp extends React.Component {
|
||||
|
||||
await this.flushState()
|
||||
},
|
||||
"auth:disabled_account": async () => {
|
||||
await SessionModel.removeToken()
|
||||
app.navigation.goAuth()
|
||||
}
|
||||
}
|
||||
|
||||
flushState = async () => {
|
||||
|
@ -240,97 +240,92 @@ const PlaylistView = (props) => {
|
||||
playlistType,
|
||||
)}
|
||||
>
|
||||
|
||||
<div className="play_info_wrapper">
|
||||
<div className="play_info">
|
||||
<div className="play_info_cover">
|
||||
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} />
|
||||
</div>
|
||||
|
||||
<div className="play_info_details">
|
||||
<div className="play_info_title">
|
||||
{
|
||||
playlist.service === "tidal" && <Icons.SiTidal />
|
||||
}
|
||||
{
|
||||
typeof playlist.title === "function" ?
|
||||
playlist.title :
|
||||
<h1>{playlist.title}</h1>
|
||||
}
|
||||
{
|
||||
!props.noHeader && <div className="play_info_wrapper">
|
||||
<div className="play_info">
|
||||
<div className="play_info_cover">
|
||||
<ImageViewer src={playlist.cover ?? playlist?.thumbnail ?? "/assets/no_song.png"} />
|
||||
</div>
|
||||
|
||||
<div className="play_info_statistics">
|
||||
{
|
||||
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item">
|
||||
{
|
||||
PlaylistTypeDecorators[playlistType]()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items
|
||||
</p>
|
||||
<div className="play_info_details">
|
||||
<div className="play_info_title">
|
||||
{
|
||||
playlist.service === "tidal" && <Icons.SiTidal />
|
||||
}
|
||||
{
|
||||
typeof playlist.title === "function" ?
|
||||
playlist.title :
|
||||
<h1>{playlist.title}</h1>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
playlist.publisher && <div className="play_info_statistics_item">
|
||||
<p
|
||||
onClick={() => {
|
||||
app.navigation.goToAccount(playlist.publisher.username)
|
||||
}}
|
||||
>
|
||||
<Icons.MdPerson />
|
||||
|
||||
Publised by <a>{playlist.publisher.username}</a>
|
||||
<div className="play_info_statistics">
|
||||
{
|
||||
playlistType && PlaylistTypeDecorators[playlistType] && <div className="play_info_statistics_item">
|
||||
{
|
||||
PlaylistTypeDecorators[playlistType]()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div className="play_info_statistics_item">
|
||||
<p>
|
||||
<Icons.MdLibraryMusic /> {props.length ?? playlist.total_length ?? playlist.list.length} Items
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
playlist.publisher && <div className="play_info_statistics_item">
|
||||
<p
|
||||
onClick={() => {
|
||||
app.navigation.goToAccount(playlist.publisher.username)
|
||||
}}
|
||||
>
|
||||
<Icons.MdPerson />
|
||||
|
||||
<div className="play_info_actions">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="rounded"
|
||||
size="large"
|
||||
onClick={handleOnClickPlaylistPlay}
|
||||
>
|
||||
<Icons.MdPlayArrow />
|
||||
Play
|
||||
</antd.Button>
|
||||
Publised by <a>{playlist.publisher.username}</a>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
{
|
||||
!props.favorite && <antd.Button
|
||||
icon={<Icons.MdFavorite />}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
playlist.description && <antd.Button
|
||||
icon={<Icons.MdInfo />}
|
||||
onClick={handleOnClickViewDetails}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
owningPlaylist &&
|
||||
<antd.Dropdown
|
||||
trigger={["click"]}
|
||||
placement="bottom"
|
||||
menu={{
|
||||
items: moreMenuItems,
|
||||
onClick: handleMoreMenuClick
|
||||
}}
|
||||
<div className="play_info_actions">
|
||||
<antd.Button
|
||||
type="primary"
|
||||
shape="rounded"
|
||||
size="large"
|
||||
onClick={handleOnClickPlaylistPlay}
|
||||
>
|
||||
<antd.Button
|
||||
icon={<Icons.MdMoreVert />}
|
||||
/>
|
||||
</antd.Dropdown>
|
||||
<Icons.MdPlayArrow />
|
||||
Play
|
||||
</antd.Button>
|
||||
|
||||
}
|
||||
{
|
||||
playlist.description && <antd.Button
|
||||
icon={<Icons.MdInfo />}
|
||||
onClick={handleOnClickViewDetails}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
owningPlaylist &&
|
||||
<antd.Dropdown
|
||||
trigger={["click"]}
|
||||
placement="bottom"
|
||||
menu={{
|
||||
items: moreMenuItems,
|
||||
onClick: handleMoreMenuClick
|
||||
}}
|
||||
>
|
||||
<antd.Button
|
||||
icon={<Icons.MdMoreVert />}
|
||||
/>
|
||||
</antd.Dropdown>
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="list">
|
||||
{
|
||||
|
@ -159,7 +159,9 @@ export default React.memo((props) => {
|
||||
props.attachments?.length > 0 && <BearCarousel
|
||||
data={props.attachments.map((attachment, index) => {
|
||||
if (typeof attachment !== "object") {
|
||||
return null
|
||||
attachment = {
|
||||
url: attachment,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
45
packages/app/src/components/SponsorsList/index.jsx
Normal file
45
packages/app/src/components/SponsorsList/index.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import React from "react"
|
||||
import { Skeleton } from "antd"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const SponsorsList = () => {
|
||||
const fetchAPI = "https://raw.githubusercontent.com/ragestudio/comty/refs/heads/master/sponsors.json"
|
||||
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [sponsors, setSponsors] = React.useState(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
fetch(fetchAPI)
|
||||
.then((response) => response.json())
|
||||
.then((data) => {
|
||||
setLoading(false)
|
||||
setSponsors(data)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return <Skeleton active />
|
||||
}
|
||||
|
||||
return <div className="sponsors_list">
|
||||
{
|
||||
sponsors && sponsors.map((sponsor, index) => {
|
||||
return <a
|
||||
key={index}
|
||||
href={sponsor.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="sponsor"
|
||||
>
|
||||
<img
|
||||
src={sponsor.promo_badge}
|
||||
alt={sponsor.name}
|
||||
/>
|
||||
</a>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default SponsorsList
|
23
packages/app/src/components/SponsorsList/index.less
Normal file
23
packages/app/src/components/SponsorsList/index.less
Normal file
@ -0,0 +1,23 @@
|
||||
.sponsors_list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
justify-content: center;
|
||||
|
||||
gap: 20px;
|
||||
|
||||
.sponsor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 5px;
|
||||
|
||||
img {
|
||||
max-width: 250px;
|
||||
max-height: 100px;
|
||||
|
||||
max-width: 250px;
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
}
|
@ -72,6 +72,10 @@ export default class APICore extends Core {
|
||||
app.eventBus.emit("session.invalid", error)
|
||||
})
|
||||
|
||||
this.client.eventBus.on("auth:disabled_account", () => {
|
||||
app.eventBus.emit("auth:disabled_account")
|
||||
})
|
||||
|
||||
// make a basic request to check if the API is available
|
||||
await this.client.baseRequest({
|
||||
method: "head",
|
||||
|
4
packages/app/src/pages/about/index.jsx
Normal file
4
packages/app/src/pages/about/index.jsx
Normal file
@ -0,0 +1,4 @@
|
||||
export default () => {
|
||||
app.location.push("/settings?tab=about")
|
||||
return null
|
||||
}
|
108
packages/app/src/pages/disable-account/index.jsx
Normal file
108
packages/app/src/pages/disable-account/index.jsx
Normal file
@ -0,0 +1,108 @@
|
||||
import React from "react"
|
||||
import { Alert, Divider, Checkbox, Button } from "antd"
|
||||
|
||||
import AuthModel from "@models/auth"
|
||||
|
||||
const DisableAccountPage = () => {
|
||||
const [confirm, setConfirm] = React.useState(false)
|
||||
|
||||
async function submit() {
|
||||
if (!confirm) {
|
||||
return null
|
||||
}
|
||||
|
||||
AuthModel.disableAccount({ confirm })
|
||||
.then(() => {
|
||||
app.message.success("Your account has been disabled. More information will be sent to your email.")
|
||||
})
|
||||
.catch(() => {
|
||||
app.message.error("Failed to disable your account.")
|
||||
})
|
||||
}
|
||||
|
||||
return <div className="flex-column gap-20 align-start w-100">
|
||||
<div className="flex-column align-start">
|
||||
<h1>Account Disablement</h1>
|
||||
<p>You are about to disable your account.</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-column gap-10 align-start">
|
||||
<Divider
|
||||
style={{
|
||||
margin: "5px 0"
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
Due to our security policy, your data is retained when your account is disabled, this retention period is 2 months, but can be extended if appropriate.
|
||||
</p>
|
||||
<p>
|
||||
Once the account hold expires, all of your data will be permanently deleted.
|
||||
</p>
|
||||
<Divider
|
||||
style={{
|
||||
margin: "5px 0"
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
This action cannot be stopped directly, you must contact support to stop this process.
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
In case an imminent deletion is necessary, you should contact support.
|
||||
</strong>
|
||||
</p>
|
||||
<Divider
|
||||
style={{
|
||||
margin: "5px 0"
|
||||
}} />
|
||||
</div>
|
||||
|
||||
<div className="flex-column gap-10 align-start w-100">
|
||||
<p>
|
||||
These are all the data that are deleted after the retention time:
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>Your account data</li>
|
||||
<li>Your profile</li>
|
||||
<li>Sessions logs</li>
|
||||
<li>All content you have created; tracks, videos, posts, images, products, events, etc...</li>
|
||||
<li>All information related to your account in our Databases</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
While your account is on hold, all of your information will remain visible, but your account will not be usable by any services or log in.
|
||||
</p>
|
||||
|
||||
<Divider
|
||||
style={{
|
||||
margin: "10px 0"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
type="warning"
|
||||
message="Some features like API keys keeps working until your account is fully deleted."
|
||||
/>
|
||||
|
||||
<div className="flex-column align-start justify-space-between gap-10 w-100">
|
||||
<Checkbox
|
||||
checked={confirm}
|
||||
onChange={(e) => setConfirm(e.target.checked)}
|
||||
>
|
||||
Im aware of this action and I want to continue.
|
||||
</Checkbox>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={submit}
|
||||
disabled={!confirm}
|
||||
>
|
||||
Disable Account
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default DisableAccountPage
|
@ -1,143 +0,0 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import PlaylistView from "@components/Music/PlaylistView"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
export default class FavoriteTracks extends React.Component {
|
||||
state = {
|
||||
error: null,
|
||||
|
||||
initialLoading: true,
|
||||
loading: false,
|
||||
|
||||
list: [],
|
||||
total_length: 0,
|
||||
|
||||
empty: false,
|
||||
hasMore: true,
|
||||
offset: 0,
|
||||
}
|
||||
|
||||
static loadLimit = 50
|
||||
|
||||
componentDidMount = async () => {
|
||||
await this.loadItems()
|
||||
}
|
||||
|
||||
onLoadMore = async () => {
|
||||
console.log(`Loading more items...`, this.state.offset)
|
||||
|
||||
const newOffset = this.state.offset + FavoriteTracks.loadLimit
|
||||
|
||||
await this.setState({
|
||||
offset: newOffset,
|
||||
})
|
||||
|
||||
await this.loadItems({
|
||||
offset: newOffset,
|
||||
})
|
||||
}
|
||||
|
||||
loadItems = async ({
|
||||
replace = false,
|
||||
offset = 0,
|
||||
limit = FavoriteTracks.loadLimit,
|
||||
} = {}) => {
|
||||
this.setState({
|
||||
loading: true,
|
||||
})
|
||||
|
||||
const result = await MusicModel.getFavouriteFolder({
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}).catch((error) => {
|
||||
this.setState({
|
||||
error: error.message,
|
||||
})
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
console.log("Loaded favorites => ", result)
|
||||
|
||||
if (result) {
|
||||
const {
|
||||
tracks,
|
||||
releases,
|
||||
playlists,
|
||||
total_length,
|
||||
} = result
|
||||
|
||||
const data = [
|
||||
...tracks.list,
|
||||
...releases.list,
|
||||
...playlists.list,
|
||||
]
|
||||
|
||||
if (total_length === 0) {
|
||||
this.setState({
|
||||
empty: true,
|
||||
hasMore: false,
|
||||
initialLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
return this.setState({
|
||||
empty: false,
|
||||
hasMore: false,
|
||||
initialLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
this.setState({
|
||||
list: data,
|
||||
})
|
||||
} else {
|
||||
this.setState({
|
||||
list: [...this.state.list, ...data],
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
total_length
|
||||
})
|
||||
}
|
||||
|
||||
this.setState({
|
||||
loading: false,
|
||||
initialLoading: false,
|
||||
})
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <antd.Result
|
||||
status="error"
|
||||
title="Error"
|
||||
subTitle={this.state.error}
|
||||
/>
|
||||
}
|
||||
|
||||
if (this.state.initialLoading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <PlaylistView
|
||||
favorite
|
||||
type="vertical"
|
||||
playlist={{
|
||||
title: "Your favorites",
|
||||
cover: "https://storage.ragestudio.net/comty-static-assets/favorite_song.png",
|
||||
list: this.state.list
|
||||
}}
|
||||
centered={app.isMobile}
|
||||
onLoadMore={this.onLoadMore}
|
||||
hasMore={this.state.hasMore}
|
||||
length={this.state.total_length}
|
||||
/>
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
import LibraryTab from "./library"
|
||||
import FavoritesTab from "./favorites"
|
||||
import ExploreTab from "./explore"
|
||||
|
||||
export default [
|
||||
@ -15,12 +14,6 @@ export default [
|
||||
icon: "MdLibraryMusic",
|
||||
component: LibraryTab,
|
||||
},
|
||||
{
|
||||
key: "favorites",
|
||||
label: "Favorites",
|
||||
icon: "MdFavoriteBorder",
|
||||
component: FavoritesTab,
|
||||
},
|
||||
{
|
||||
key: "radio",
|
||||
label: "Radio",
|
||||
|
@ -1,189 +1,40 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import Image from "@components/Image"
|
||||
import { Icons } from "@components/Icons"
|
||||
import OpenPlaylistCreator from "@components/Music/PlaylistCreator"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
import TracksLibraryView from "./views/tracks"
|
||||
import PlaylistLibraryView from "./views/playlists"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ReleaseTypeDecorators = {
|
||||
"user": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Playlist
|
||||
</p>,
|
||||
"playlist": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Playlist
|
||||
</p>,
|
||||
"editorial": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Official Playlist
|
||||
</p>,
|
||||
"single": () => <p >
|
||||
<Icons.MdMusicNote />
|
||||
Single
|
||||
</p>,
|
||||
"album": () => <p >
|
||||
<Icons.MdAlbum />
|
||||
Album
|
||||
</p>,
|
||||
"ep": () => <p >
|
||||
<Icons.MdAlbum />
|
||||
EP
|
||||
</p>,
|
||||
"mix": () => <p >
|
||||
<Icons.MdMusicNote />
|
||||
Mix
|
||||
</p>,
|
||||
const TabToView = {
|
||||
tracks: TracksLibraryView,
|
||||
playlist: PlaylistLibraryView,
|
||||
releases: PlaylistLibraryView,
|
||||
}
|
||||
|
||||
function isNotAPlaylist(type) {
|
||||
return type === "album" || type === "ep" || type === "mix" || type === "single"
|
||||
}
|
||||
|
||||
const PlaylistItem = (props) => {
|
||||
const data = props.data ?? {}
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (typeof props.onClick === "function") {
|
||||
props.onClick(data)
|
||||
}
|
||||
|
||||
if (props.type !== "action") {
|
||||
if (data.service) {
|
||||
return app.navigation.goToPlaylist(`${data._id}?service=${data.service}`)
|
||||
}
|
||||
|
||||
return app.navigation.goToPlaylist(data._id)
|
||||
}
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"playlist_item",
|
||||
{
|
||||
["action"]: props.type === "action",
|
||||
["release"]: isNotAPlaylist(data.type),
|
||||
}
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<div className="playlist_item_icon">
|
||||
{
|
||||
React.isValidElement(data.icon)
|
||||
? <div className="playlist_item_icon_svg">
|
||||
{data.icon}
|
||||
</div>
|
||||
: <Image
|
||||
src={data.icon}
|
||||
alt="playlist icon"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="playlist_item_info">
|
||||
<div className="playlist_item_info_title">
|
||||
<h1>
|
||||
{
|
||||
data.service === "tidal" && <Icons.SiTidal />
|
||||
}
|
||||
{
|
||||
data.title ?? "Unnamed playlist"
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{
|
||||
data.owner && <div className="playlist_item_info_owner">
|
||||
<h4>
|
||||
{
|
||||
data.owner
|
||||
}
|
||||
</h4>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
data.description && <div className="playlist_item_info_description">
|
||||
<p>
|
||||
{
|
||||
data.description
|
||||
}
|
||||
</p>
|
||||
|
||||
{
|
||||
ReleaseTypeDecorators[String(data.type).toLowerCase()] && ReleaseTypeDecorators[String(data.type).toLowerCase()](props)
|
||||
}
|
||||
|
||||
{
|
||||
data.public
|
||||
? <p>
|
||||
<Icons.MdVisibility />
|
||||
Public
|
||||
</p>
|
||||
|
||||
: <p>
|
||||
<Icons.MdVisibilityOff />
|
||||
Private
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const OwnPlaylists = (props) => {
|
||||
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists)
|
||||
|
||||
if (E_Playlists) {
|
||||
console.error(E_Playlists)
|
||||
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle="We are sorry, but we could not load your playlists. Please try again later."
|
||||
/>
|
||||
}
|
||||
|
||||
if (L_Playlists) {
|
||||
return <antd.Skeleton />
|
||||
}
|
||||
|
||||
return <div className="own_playlists">
|
||||
<PlaylistItem
|
||||
type="action"
|
||||
data={{
|
||||
icon: <Icons.MdPlaylistAdd />,
|
||||
title: "Create new",
|
||||
}}
|
||||
onClick={OpenPlaylistCreator}
|
||||
/>
|
||||
|
||||
{
|
||||
R_Playlists.items.map((playlist) => {
|
||||
playlist.icon = playlist.cover ?? playlist.thumbnail
|
||||
playlist.description = `${playlist.numberOfTracks ?? playlist.list.length} tracks`
|
||||
|
||||
return <PlaylistItem
|
||||
key={playlist.id}
|
||||
data={playlist}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
const TabToHeader = {
|
||||
tracks: {
|
||||
icon: <Icons.MdMusicNote />,
|
||||
label: "Tracks",
|
||||
},
|
||||
playlist: {
|
||||
icon: <Icons.MdPlaylistPlay />,
|
||||
label: "Playlists",
|
||||
},
|
||||
}
|
||||
|
||||
const Library = (props) => {
|
||||
const [selectedTab, setSelectedTab] = React.useState("tracks")
|
||||
|
||||
return <div className="music-library">
|
||||
<div className="music-library_header">
|
||||
<h1>Library</h1>
|
||||
|
||||
<antd.Segmented
|
||||
value={selectedTab}
|
||||
onChange={setSelectedTab}
|
||||
options={[
|
||||
{
|
||||
value: "tracks",
|
||||
@ -195,18 +46,18 @@ const Library = (props) => {
|
||||
label: "Playlists",
|
||||
icon: <Icons.MdPlaylistPlay />
|
||||
},
|
||||
{
|
||||
value: "releases",
|
||||
label: "Releases",
|
||||
icon: <Icons.MdPlaylistPlay />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<PlaylistItem
|
||||
type="action"
|
||||
data={{
|
||||
icon: <Icons.MdPlaylistAdd />,
|
||||
title: "Create new",
|
||||
}}
|
||||
onClick={OpenPlaylistCreator}
|
||||
/>
|
||||
<OwnPlaylists />
|
||||
|
||||
{
|
||||
selectedTab && TabToView[selectedTab] && React.createElement(TabToView[selectedTab])
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
@ -1,170 +1,3 @@
|
||||
@playlist_item_icon_size: 50px;
|
||||
|
||||
.playlist_item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 70px;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&.release {
|
||||
.playlist_item_icon {
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
.playlist_item_icon {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: @playlist_item_icon_size;
|
||||
height: @playlist_item_icon_size;
|
||||
|
||||
min-width: @playlist_item_icon_size;
|
||||
min-height: @playlist_item_icon_size;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
width: @playlist_item_icon_size;
|
||||
height: @playlist_item_icon_size;
|
||||
|
||||
min-width: @playlist_item_icon_size;
|
||||
min-height: @playlist_item_icon_size;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.playlist_item_icon_svg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
svg {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
//align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
width: 90%;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.playlist_item_info_title {
|
||||
display: inline;
|
||||
|
||||
|
||||
font-size: 0.8rem;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info_owner {
|
||||
display: inline;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 0.7rem;
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info_description {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
font-size: 0.7rem;
|
||||
|
||||
p {
|
||||
font-weight: 500;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
svg {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
span {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.own_playlists {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.music-library {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -184,4 +17,11 @@
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.music-library-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: 15px;
|
||||
}
|
||||
}
|
@ -0,0 +1,181 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
import classnames from "classnames"
|
||||
|
||||
import Image from "@components/Image"
|
||||
import { Icons } from "@components/Icons"
|
||||
import OpenPlaylistCreator from "@components/Music/PlaylistCreator"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ReleaseTypeDecorators = {
|
||||
"user": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Playlist
|
||||
</p>,
|
||||
"playlist": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Playlist
|
||||
</p>,
|
||||
"editorial": () => <p >
|
||||
<Icons.MdPlaylistAdd />
|
||||
Official Playlist
|
||||
</p>,
|
||||
"single": () => <p >
|
||||
<Icons.MdMusicNote />
|
||||
Single
|
||||
</p>,
|
||||
"album": () => <p >
|
||||
<Icons.MdAlbum />
|
||||
Album
|
||||
</p>,
|
||||
"ep": () => <p >
|
||||
<Icons.MdAlbum />
|
||||
EP
|
||||
</p>,
|
||||
"mix": () => <p >
|
||||
<Icons.MdMusicNote />
|
||||
Mix
|
||||
</p>,
|
||||
}
|
||||
|
||||
function isNotAPlaylist(type) {
|
||||
return type === "album" || type === "ep" || type === "mix" || type === "single"
|
||||
}
|
||||
|
||||
const PlaylistItem = (props) => {
|
||||
const data = props.data ?? {}
|
||||
|
||||
const handleOnClick = () => {
|
||||
if (typeof props.onClick === "function") {
|
||||
props.onClick(data)
|
||||
}
|
||||
|
||||
if (props.type !== "action") {
|
||||
if (data.service) {
|
||||
return app.navigation.goToPlaylist(`${data._id}?service=${data.service}`)
|
||||
}
|
||||
|
||||
return app.navigation.goToPlaylist(data._id)
|
||||
}
|
||||
}
|
||||
|
||||
return <div
|
||||
className={classnames(
|
||||
"playlist_item",
|
||||
{
|
||||
["action"]: props.type === "action",
|
||||
["release"]: isNotAPlaylist(data.type),
|
||||
}
|
||||
)}
|
||||
onClick={handleOnClick}
|
||||
>
|
||||
<div className="playlist_item_icon">
|
||||
{
|
||||
React.isValidElement(data.icon)
|
||||
? <div className="playlist_item_icon_svg">
|
||||
{data.icon}
|
||||
</div>
|
||||
: <Image
|
||||
src={data.icon}
|
||||
alt="playlist icon"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="playlist_item_info">
|
||||
<div className="playlist_item_info_title">
|
||||
<h1>
|
||||
{
|
||||
data.service === "tidal" && <Icons.SiTidal />
|
||||
}
|
||||
{
|
||||
data.title ?? "Unnamed playlist"
|
||||
}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{
|
||||
data.owner && <div className="playlist_item_info_owner">
|
||||
<h4>
|
||||
{
|
||||
data.owner
|
||||
}
|
||||
</h4>
|
||||
</div>
|
||||
}
|
||||
|
||||
{
|
||||
data.description && <div className="playlist_item_info_description">
|
||||
<p>
|
||||
{
|
||||
data.description
|
||||
}
|
||||
</p>
|
||||
|
||||
{
|
||||
ReleaseTypeDecorators[String(data.type).toLowerCase()] && ReleaseTypeDecorators[String(data.type).toLowerCase()](props)
|
||||
}
|
||||
|
||||
{
|
||||
data.public
|
||||
? <p>
|
||||
<Icons.MdVisibility />
|
||||
Public
|
||||
</p>
|
||||
|
||||
: <p>
|
||||
<Icons.MdVisibilityOff />
|
||||
Private
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const PlaylistLibraryView = (props) => {
|
||||
const [L_Playlists, R_Playlists, E_Playlists, M_Playlists] = app.cores.api.useRequest(MusicModel.getFavoritePlaylists)
|
||||
|
||||
if (E_Playlists) {
|
||||
console.error(E_Playlists)
|
||||
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle="We are sorry, but we could not load your playlists. Please try again later."
|
||||
/>
|
||||
}
|
||||
|
||||
if (L_Playlists) {
|
||||
return <antd.Skeleton />
|
||||
}
|
||||
|
||||
return <div className="own_playlists">
|
||||
<PlaylistItem
|
||||
type="action"
|
||||
data={{
|
||||
icon: <Icons.MdPlaylistAdd />,
|
||||
title: "Create new",
|
||||
}}
|
||||
onClick={OpenPlaylistCreator}
|
||||
/>
|
||||
|
||||
{
|
||||
R_Playlists.items.map((playlist) => {
|
||||
playlist.icon = playlist.cover ?? playlist.thumbnail
|
||||
playlist.description = `${playlist.numberOfTracks ?? playlist.list.length} tracks`
|
||||
|
||||
return <PlaylistItem
|
||||
key={playlist.id}
|
||||
data={playlist}
|
||||
/>
|
||||
})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
export default PlaylistLibraryView
|
@ -0,0 +1,166 @@
|
||||
@playlist_item_icon_size: 50px;
|
||||
|
||||
.playlist_item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
width: 100%;
|
||||
|
||||
height: 70px;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
padding: 10px;
|
||||
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
&.release {
|
||||
.playlist_item_icon {
|
||||
img {
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
.playlist_item_icon {
|
||||
color: var(--colorPrimary);
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: @playlist_item_icon_size;
|
||||
height: @playlist_item_icon_size;
|
||||
|
||||
min-width: @playlist_item_icon_size;
|
||||
min-height: @playlist_item_icon_size;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
border-radius: 12px;
|
||||
|
||||
img {
|
||||
width: @playlist_item_icon_size;
|
||||
height: @playlist_item_icon_size;
|
||||
|
||||
min-width: @playlist_item_icon_size;
|
||||
min-height: @playlist_item_icon_size;
|
||||
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.playlist_item_icon_svg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--background-color-accent);
|
||||
|
||||
svg {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
//align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
height: 100%;
|
||||
width: 90%;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.playlist_item_info_title {
|
||||
display: inline;
|
||||
|
||||
|
||||
font-size: 0.8rem;
|
||||
|
||||
text-overflow: ellipsis;
|
||||
|
||||
h1 {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info_owner {
|
||||
display: inline;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
font-size: 0.7rem;
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.playlist_item_info_description {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: 10px;
|
||||
|
||||
font-size: 0.7rem;
|
||||
|
||||
p {
|
||||
font-weight: 500;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
text-transform: uppercase;
|
||||
|
||||
svg {
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
p,
|
||||
span {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.own_playlists {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
width: 100%;
|
||||
|
||||
gap: 10px;
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import React from "react"
|
||||
import * as antd from "antd"
|
||||
|
||||
import PlaylistView from "@components/Music/PlaylistView"
|
||||
|
||||
import MusicModel from "@models/music"
|
||||
|
||||
const loadLimit = 50
|
||||
|
||||
const TracksLibraryView = () => {
|
||||
const [offset, setOffset] = React.useState(0)
|
||||
const [list, setList] = React.useState([])
|
||||
const [hasMore, setHasMore] = React.useState(true)
|
||||
const [initialLoading, setInitialLoading] = React.useState(true)
|
||||
|
||||
const [L_Favourites, R_Favourites, E_Favourites, M_Favourites] = app.cores.api.useRequest(MusicModel.getFavouriteFolder, {
|
||||
offset: offset,
|
||||
limit: loadLimit,
|
||||
})
|
||||
|
||||
async function onLoadMore() {
|
||||
const newOffset = offset + loadLimit
|
||||
|
||||
setOffset(newOffset)
|
||||
|
||||
M_Favourites({
|
||||
offset: newOffset,
|
||||
limit: loadLimit,
|
||||
})
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (R_Favourites && R_Favourites.tracks) {
|
||||
if (initialLoading === true) {
|
||||
setInitialLoading(false)
|
||||
}
|
||||
|
||||
if (R_Favourites.tracks.list.length === 0) {
|
||||
setHasMore(false)
|
||||
} else {
|
||||
setList((prev) => {
|
||||
prev = [
|
||||
...prev,
|
||||
...R_Favourites.tracks.list,
|
||||
]
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [R_Favourites])
|
||||
|
||||
if (E_Favourites) {
|
||||
return <antd.Result
|
||||
status="warning"
|
||||
title="Failed to load"
|
||||
subTitle={E_Favourites}
|
||||
/>
|
||||
}
|
||||
|
||||
if (initialLoading) {
|
||||
return <antd.Skeleton active />
|
||||
}
|
||||
|
||||
return <PlaylistView
|
||||
noHeader
|
||||
loading={L_Favourites}
|
||||
type="vertical"
|
||||
playlist={{
|
||||
list: list
|
||||
}}
|
||||
onLoadMore={onLoadMore}
|
||||
hasMore={hasMore}
|
||||
length={R_Favourites.tracks.total_length}
|
||||
/>
|
||||
}
|
||||
|
||||
export default TracksLibraryView
|
@ -7,6 +7,12 @@ import MyReleasesList from "@components/MusicStudio/MyReleasesList"
|
||||
|
||||
import "./index.less"
|
||||
|
||||
const ReleasesAnalytics = () => {
|
||||
return <div>
|
||||
<h1>Analytics</h1>
|
||||
</div>
|
||||
}
|
||||
|
||||
const MusicStudioPage = (props) => {
|
||||
return <div
|
||||
className="music-studio-page"
|
||||
@ -25,6 +31,8 @@ const MusicStudioPage = (props) => {
|
||||
</antd.Button>
|
||||
</div>
|
||||
|
||||
<ReleasesAnalytics />
|
||||
|
||||
<MyReleasesList />
|
||||
</div>
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import * as antd from "antd"
|
||||
|
||||
import { Icons } from "@components/Icons"
|
||||
import LatencyIndicator from "@components/PerformanceIndicators/latency"
|
||||
import SponsorsList from "@components/SponsorsList"
|
||||
|
||||
import config from "@config"
|
||||
|
||||
@ -157,6 +158,11 @@ export default {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<h3>Thanks to our sponsors</h3>
|
||||
<SponsorsList />
|
||||
</div>
|
||||
|
||||
<div className="group">
|
||||
<div className="inline_field">
|
||||
<div className="field_header">
|
||||
|
@ -32,6 +32,21 @@ export default {
|
||||
description: "Manage your active sessions",
|
||||
icon: "FiMonitor",
|
||||
component: loadable(() => import("../components/sessions")),
|
||||
},
|
||||
{
|
||||
id: "disable-account",
|
||||
group: "security.account",
|
||||
title: "Disable Account",
|
||||
description: "Disable your account",
|
||||
icon: "FiUserX",
|
||||
component: "Button",
|
||||
props: {
|
||||
danger: true,
|
||||
children: "Disable",
|
||||
onClick: () => {
|
||||
app.location.push("/disable-account")
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
@ -1,3 +1,15 @@
|
||||
// DIMENSIONS
|
||||
|
||||
// WIDTH
|
||||
.w-100 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// HEIGHT
|
||||
.h-100 {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// FLEX
|
||||
.flex-row {
|
||||
display: flex;
|
||||
@ -50,6 +62,11 @@
|
||||
}
|
||||
|
||||
// GAPS
|
||||
.gap-20,
|
||||
.gap20 {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.gap-10,
|
||||
.gap10 {
|
||||
gap: 10px;
|
||||
|
@ -68,6 +68,10 @@ export default {
|
||||
activated: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
}
|
||||
}
|
@ -389,8 +389,8 @@ export default class Gateway {
|
||||
async initialize() {
|
||||
onExit(this.onGatewayExit)
|
||||
|
||||
process.stdout.setMaxListeners(50)
|
||||
process.stderr.setMaxListeners(50)
|
||||
process.stdout.setMaxListeners(150)
|
||||
process.stderr.setMaxListeners(150)
|
||||
|
||||
this.services = await scanServices()
|
||||
this.proxy = new Proxy()
|
||||
|
@ -8,4 +8,5 @@ export default class Account {
|
||||
static deleteSession = require("./methods/deleteSession").default
|
||||
static sendActivationCode = require("./methods/sendActivationCode").default
|
||||
static activateAccount = require("./methods/activateAccount").default
|
||||
static disableAccount = require("./methods/disableAccount").default
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
import { User } from "@db_models"
|
||||
|
||||
export default async (payload) => {
|
||||
const { user_id } = payload
|
||||
|
||||
if (!user_id) {
|
||||
throw new OperationError(400, "Missing user_id")
|
||||
}
|
||||
|
||||
let user = await User.findOne({
|
||||
_id: user_id,
|
||||
}).select("+email")
|
||||
|
||||
if (!user) {
|
||||
throw new OperationError(404, "User not found")
|
||||
}
|
||||
|
||||
user = await User.findOneAndUpdate(
|
||||
{
|
||||
_id: user._id.toString(),
|
||||
},
|
||||
{
|
||||
disabled: true
|
||||
},
|
||||
)
|
||||
|
||||
return user.toObject()
|
||||
}
|
@ -14,6 +14,10 @@ export default async ({ username, password, hash }, user) => {
|
||||
throw new OperationError(401, "User not found")
|
||||
}
|
||||
|
||||
if (user.disabled == true) {
|
||||
throw new OperationError(401, "User is disabled")
|
||||
}
|
||||
|
||||
if (typeof hash !== "undefined") {
|
||||
if (user.password !== hash) {
|
||||
throw new OperationError(401, "Invalid credentials")
|
||||
|
@ -0,0 +1,17 @@
|
||||
import AccountClass from "@classes/account"
|
||||
import { OperationLog } from "@db_models"
|
||||
|
||||
export default {
|
||||
middlewares: ["withAuthentication"],
|
||||
fn: async (req) => {
|
||||
const user_id = req.auth.session.user_id
|
||||
|
||||
await OperationLog.create({
|
||||
user_id: user_id,
|
||||
type: "disable_account",
|
||||
date: Date.now()
|
||||
})
|
||||
|
||||
return await AccountClass.disableAccount({ user_id })
|
||||
}
|
||||
}
|
@ -31,6 +31,7 @@ export default async (req, res) => {
|
||||
|
||||
if (user.activated === false) {
|
||||
return res.status(401).json({
|
||||
code: user.email,
|
||||
user_id: user._id.toString(),
|
||||
activation_required: true,
|
||||
})
|
||||
|
@ -38,7 +38,7 @@ export default {
|
||||
parentDir: req.auth.session.user_id,
|
||||
source: localFilepath,
|
||||
service: providerType,
|
||||
useCompression: req.headers["use-compression"] ?? true,
|
||||
useCompression: ToBoolean(req.headers["use-compression"]) ?? true,
|
||||
})
|
||||
|
||||
fs.promises.rm(tmpPath, { recursive: true, force: true })
|
||||
|
@ -47,6 +47,7 @@ export default (input, params = defaultParams) => {
|
||||
const commands = {
|
||||
input: input,
|
||||
...params,
|
||||
preset: "ultrafast",
|
||||
output: outputFilepath,
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,23 @@ export default async (payload = {}) => {
|
||||
let { user_id, message, attachments, timestamp, reply_to, poll_options } = payload
|
||||
|
||||
// check if is a Array and have at least one element
|
||||
const isAttachmentsValid = Array.isArray(attachments) && attachments.length > 0
|
||||
const isAttachmentArray = Array.isArray(attachments) && attachments.length > 0
|
||||
|
||||
if (!isAttachmentsValid && !message) {
|
||||
if (!isAttachmentArray && !message) {
|
||||
throw new OperationError(400, "Cannot create a post without message or attachments")
|
||||
}
|
||||
|
||||
// fix attachments with url strings
|
||||
attachments = attachments.map((attachment) => {
|
||||
if (typeof attachment === "string") {
|
||||
attachment = {
|
||||
url: attachment,
|
||||
}
|
||||
}
|
||||
|
||||
return attachment
|
||||
})
|
||||
|
||||
if (!timestamp) {
|
||||
timestamp = DateTime.local().toISO()
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user