Made file uploads display in drafts
This commit is contained in:
@@ -75,6 +75,7 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
const [text, setText] = useState("");
|
const [text, setText] = useState("");
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
const [pendingFile, setPendingFile] = useState<File | null>(null);
|
||||||
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
const [mediaId, setMediaId] = useState<string | null>(null);
|
const [mediaId, setMediaId] = useState<string | null>(null);
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||||
@@ -100,6 +101,10 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
setMediaId(null);
|
setMediaId(null);
|
||||||
setPendingFile(null);
|
setPendingFile(null);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
|
if (previewUrl) {
|
||||||
|
URL.revokeObjectURL(previewUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
|
}
|
||||||
onSuccess?.();
|
onSuccess?.();
|
||||||
},
|
},
|
||||||
onError: (e: Error) => setError(e.message),
|
onError: (e: Error) => setError(e.message),
|
||||||
@@ -110,7 +115,11 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
// Reset the input so the same file can be re-selected after removal
|
// Reset the input so the same file can be re-selected after removal
|
||||||
e.target.value = "";
|
e.target.value = "";
|
||||||
|
// Revoke any previous object URL to avoid memory leaks
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
|
const localUrl = URL.createObjectURL(file);
|
||||||
setPendingFile(file);
|
setPendingFile(file);
|
||||||
|
setPreviewUrl(localUrl);
|
||||||
setMediaId(null);
|
setMediaId(null);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
@@ -120,13 +129,17 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
if ("error" in result) {
|
if ("error" in result) {
|
||||||
setUploadError(result.error);
|
setUploadError(result.error);
|
||||||
setPendingFile(null);
|
setPendingFile(null);
|
||||||
|
URL.revokeObjectURL(localUrl);
|
||||||
|
setPreviewUrl(null);
|
||||||
} else {
|
} else {
|
||||||
setMediaId(result.mediaId);
|
setMediaId(result.mediaId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeAttachment() {
|
function removeAttachment() {
|
||||||
|
if (previewUrl) URL.revokeObjectURL(previewUrl);
|
||||||
setPendingFile(null);
|
setPendingFile(null);
|
||||||
|
setPreviewUrl(null);
|
||||||
setMediaId(null);
|
setMediaId(null);
|
||||||
setUploadError(null);
|
setUploadError(null);
|
||||||
}
|
}
|
||||||
@@ -173,6 +186,47 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
rows={2}
|
rows={2}
|
||||||
maxLength={MAX + 1}
|
maxLength={MAX + 1}
|
||||||
/>
|
/>
|
||||||
|
{previewUrl && pendingFile && (
|
||||||
|
<div style={{ position: "relative", marginTop: "0.5rem", display: "inline-block" }}>
|
||||||
|
{/\.(mp4|mov)$/i.test(pendingFile.name) ? (
|
||||||
|
<video
|
||||||
|
src={previewUrl}
|
||||||
|
controls
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt="attachment preview"
|
||||||
|
style={{ maxWidth: "100%", maxHeight: "200px", borderRadius: "0.5rem", display: "block" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{uploading && (
|
||||||
|
<div style={{
|
||||||
|
position: "absolute", inset: 0, background: "rgba(0,0,0,0.4)",
|
||||||
|
borderRadius: "0.5rem", display: "flex", alignItems: "center", justifyContent: "center",
|
||||||
|
color: "#fff", fontSize: "0.75rem"
|
||||||
|
}}>
|
||||||
|
Uploading…
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={removeAttachment}
|
||||||
|
style={{
|
||||||
|
position: "absolute", top: "4px", right: "4px",
|
||||||
|
background: "rgba(0,0,0,0.6)", border: "none", borderRadius: "50%",
|
||||||
|
width: "20px", height: "20px", cursor: "pointer",
|
||||||
|
color: "#fff", fontSize: "12px", lineHeight: 1,
|
||||||
|
display: "flex", alignItems: "center", justifyContent: "center"
|
||||||
|
}}
|
||||||
|
title="Remove attachment"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{uploadError && <p className="mx-compose-error">{uploadError}</p>}
|
||||||
{error && <p className="mx-compose-error">{error}</p>}
|
{error && <p className="mx-compose-error">{error}</p>}
|
||||||
<div className="mx-compose-footer">
|
<div className="mx-compose-footer">
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem" }}>
|
||||||
@@ -195,24 +249,10 @@ function ComposeTweet({ onSuccess }: { onSuccess?: () => void }) {
|
|||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
{uploading && (
|
{uploading && (
|
||||||
<span style={{ fontSize: "0.75rem", color: "var(--mx-muted)" }}>Uploading…</span>
|
<span style={{ fontSize: "0.75rem", color: "var(--mx-muted)" }}>
|
||||||
)}
|
{pendingFile?.name}
|
||||||
{pendingFile && !uploading && (
|
|
||||||
<span style={{ fontSize: "0.75rem", color: "var(--mx-muted)", display: "flex", alignItems: "center", gap: "0.25rem" }}>
|
|
||||||
{pendingFile.name}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={removeAttachment}
|
|
||||||
style={{ background: "none", border: "none", cursor: "pointer", padding: "0 2px", color: "inherit" }}
|
|
||||||
title="Remove attachment"
|
|
||||||
>
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{uploadError && (
|
|
||||||
<span style={{ fontSize: "0.75rem", color: "#ef4444" }}>{uploadError}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-compose-actions">
|
<div className="mx-compose-actions">
|
||||||
<CharCount current={text.length} max={MAX} />
|
<CharCount current={text.length} max={MAX} />
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ defmodule Mixer.Posts.MediaUploader do
|
|||||||
def storage_dir(_version, {_file, scope}), do: "uploads/media/#{scope.id}"
|
def storage_dir(_version, {_file, scope}), do: "uploads/media/#{scope.id}"
|
||||||
|
|
||||||
def filename(_version, {file, _scope}) do
|
def filename(_version, {file, _scope}) do
|
||||||
Path.basename(file.file_name, Path.extname(file.file_name))
|
Path.basename(file.file_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
def s3_object_headers(_version, {file, _scope}) do
|
def s3_object_headers(_version, {file, _scope}) do
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ defmodule Mixer.Posts.Tweet do
|
|||||||
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
|
Ash.Changeset.after_action(changeset, fn _changeset, tweet ->
|
||||||
Mixer.Posts.Media
|
Mixer.Posts.Media
|
||||||
|> Ash.get!(media_id, authorize?: false)
|
|> Ash.get!(media_id, authorize?: false)
|
||||||
|> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id})
|
|> Ash.Changeset.for_update(:link_to_tweet, %{tweet_id: tweet.id}, actor: context.actor)
|
||||||
|> Ash.update!(actor: context.actor)
|
|> Ash.update!()
|
||||||
|
|
||||||
{:ok, tweet}
|
{:ok, tweet}
|
||||||
end)
|
end)
|
||||||
|
|||||||
Reference in New Issue
Block a user