Upload profile pictures instead of doing base64
This commit is contained in:
@@ -14,6 +14,7 @@ dependencies {
|
|||||||
implementation project(':capacitor-keyboard')
|
implementation project(':capacitor-keyboard')
|
||||||
implementation project(':capacitor-preferences')
|
implementation project(':capacitor-preferences')
|
||||||
implementation project(':capacitor-push-notifications')
|
implementation project(':capacitor-push-notifications')
|
||||||
|
implementation project(':capawesome-capacitor-android-dark-mode-support')
|
||||||
implementation project(':capawesome-capacitor-badge')
|
implementation project(':capawesome-capacitor-badge')
|
||||||
implementation project(':nostr-signer-capacitor-plugin')
|
implementation project(':nostr-signer-capacitor-plugin')
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@
|
|||||||
include ':capacitor-push-notifications'
|
include ':capacitor-push-notifications'
|
||||||
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/.pnpm/@capacitor+push-notifications@7.0.1_@capacitor+core@7.2.0/node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
|
include ':capawesome-capacitor-android-dark-mode-support'
|
||||||
|
project(':capawesome-capacitor-android-dark-mode-support').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-android-dark-mode-support@7.0.0_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-android-dark-mode-support/android')
|
||||||
|
|
||||||
include ':capawesome-capacitor-badge'
|
include ':capawesome-capacitor-badge'
|
||||||
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge/android')
|
project(':capawesome-capacitor-badge').projectDir = new File('../node_modules/.pnpm/@capawesome+capacitor-badge@7.0.1_@capacitor+core@7.2.0/node_modules/@capawesome/capacitor-badge/android')
|
||||||
|
|
||||||
|
|||||||
+24
-14
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {randomId} from "@welshman/lib"
|
import {randomId, call} from "@welshman/lib"
|
||||||
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
|
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import {uploadFile} from "@app/core/commands"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
file?: File | undefined
|
file?: File | undefined
|
||||||
@@ -47,20 +48,29 @@
|
|||||||
let initialUrl = $state(url)
|
let initialUrl = $state(url)
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (file) {
|
call(async () => {
|
||||||
const reader = new FileReader()
|
if (file) {
|
||||||
|
const {result} = await uploadFile(file)
|
||||||
|
|
||||||
reader.addEventListener(
|
if (result?.url) {
|
||||||
"load",
|
url = result.url
|
||||||
() => {
|
} else {
|
||||||
url = reader.result as string
|
const reader = new FileReader()
|
||||||
},
|
|
||||||
false,
|
reader.addEventListener(
|
||||||
)
|
"load",
|
||||||
reader.readAsDataURL(file)
|
() => {
|
||||||
} else {
|
url = reader.result as string
|
||||||
url = initialUrl
|
},
|
||||||
}
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.readAsDataURL(file)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = initialUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import FieldInline from "@lib/components/FieldInline.svelte"
|
import FieldInline from "@lib/components/FieldInline.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
|
||||||
import InfoHandle from "@app/components/InfoHandle.svelte"
|
import InfoHandle from "@app/components/InfoHandle.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import InputProfilePicture from "@lib/components/InputProfilePicture.svelte"
|
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Field from "@lib/components/Field.svelte"
|
import Field from "@lib/components/Field.svelte"
|
||||||
import FireMinimalistic from "@assets/icons/fire-minimalistic.svg?dataurl"
|
import FireMinimalistic from "@assets/icons/fire-minimalistic.svg?dataurl"
|
||||||
@@ -11,6 +10,7 @@
|
|||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import InfoRelay from "@app/components/InfoRelay.svelte"
|
import InfoRelay from "@app/components/InfoRelay.svelte"
|
||||||
|
import InputProfilePicture from "@app/components/InputProfilePicture.svelte"
|
||||||
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
import SpaceCreateFinish from "@app/components/SpaceCreateFinish.svelte"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as nip19 from "nostr-tools/nip19"
|
|||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import type {Override, MakeOptional} from "@welshman/lib"
|
import type {Override, MakeOptional} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
|
sha256,
|
||||||
randomId,
|
randomId,
|
||||||
append,
|
append,
|
||||||
remove,
|
remove,
|
||||||
@@ -16,7 +17,8 @@ import {
|
|||||||
fromPairs,
|
fromPairs,
|
||||||
last,
|
last,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {decrypt} from "@welshman/signer"
|
import {decrypt, Nip01Signer} from "@welshman/signer"
|
||||||
|
import type {UploadTask} from "@welshman/editor"
|
||||||
import type {Feed} from "@welshman/feeds"
|
import type {Feed} from "@welshman/feeds"
|
||||||
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} from "@welshman/feeds"
|
import {makeIntersectionFeed, feedFromFilters, makeRelayFeed} from "@welshman/feeds"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
@@ -53,6 +55,10 @@ import {
|
|||||||
RelayMode,
|
RelayMode,
|
||||||
getAddress,
|
getAddress,
|
||||||
getTagValue,
|
getTagValue,
|
||||||
|
getTagValues,
|
||||||
|
uploadBlob,
|
||||||
|
encryptFile,
|
||||||
|
makeBlossomAuthEvent,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
@@ -75,7 +81,9 @@ import {
|
|||||||
tagEventForQuote,
|
tagEventForQuote,
|
||||||
waitForThunkError,
|
waitForThunkError,
|
||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
|
userBlossomServers,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
|
import {compressFile} from "@src/lib/html"
|
||||||
import type {SettingsValues, Alert} from "@app/core/state"
|
import type {SettingsValues, Alert} from "@app/core/state"
|
||||||
import {
|
import {
|
||||||
SETTINGS,
|
SETTINGS,
|
||||||
@@ -649,3 +657,77 @@ export const enableGiftWraps = () => {
|
|||||||
ensureUnwrapped(event)
|
ensureUnwrapped(event)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File upload
|
||||||
|
|
||||||
|
export const getBlossomServer = () => {
|
||||||
|
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
|
||||||
|
|
||||||
|
for (const url of userUrls) {
|
||||||
|
return url.replace(/^ws/, "http")
|
||||||
|
}
|
||||||
|
|
||||||
|
return "https://cdn.satellite.earth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UploadFileOptions = {
|
||||||
|
encrypt?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UploadFileResult = {
|
||||||
|
error?: string
|
||||||
|
result?: UploadTask
|
||||||
|
}
|
||||||
|
|
||||||
|
export const uploadFile = async (file: File, options: UploadFileOptions = {}) => {
|
||||||
|
const {name, type} = file
|
||||||
|
|
||||||
|
if (!type.match("image/(webp|gif)")) {
|
||||||
|
file = await compressFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags: string[][] = []
|
||||||
|
|
||||||
|
if (options.encrypt) {
|
||||||
|
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
|
||||||
|
|
||||||
|
tags.push(
|
||||||
|
["decryption-key", key],
|
||||||
|
["decryption-nonce", nonce],
|
||||||
|
["encryption-algorithm", algorithm],
|
||||||
|
)
|
||||||
|
|
||||||
|
file = new File([new Blob([ciphertext])], name, {
|
||||||
|
type: "application/octet-stream",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = getBlossomServer()
|
||||||
|
const hashes = [await sha256(await file.arrayBuffer())]
|
||||||
|
const $signer = signer.get() || Nip01Signer.ephemeral()
|
||||||
|
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
|
||||||
|
const authEvent = await $signer.sign(authTemplate)
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await uploadBlob(server, file, {authEvent})
|
||||||
|
const text = await res.text()
|
||||||
|
|
||||||
|
let {uploaded, url, ...task} = parseJson(text) || {}
|
||||||
|
|
||||||
|
if (!uploaded) {
|
||||||
|
return {error: text}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always append file extension if missing
|
||||||
|
if (new URL(url).pathname.split(".").length === 1) {
|
||||||
|
url += "." + type.split("/")[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {...task, tags, url}
|
||||||
|
|
||||||
|
return {result}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
return {error: e.toString()}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+7
-76
@@ -1,33 +1,14 @@
|
|||||||
import {mount} from "svelte"
|
import {mount} from "svelte"
|
||||||
import type {Writable} from "svelte/store"
|
import type {Writable} from "svelte/store"
|
||||||
import {get} from "svelte/store"
|
import {get} from "svelte/store"
|
||||||
import {sha256, parseJson} from "@welshman/lib"
|
|
||||||
import {
|
|
||||||
getTagValues,
|
|
||||||
encryptFile,
|
|
||||||
uploadBlob,
|
|
||||||
makeBlossomAuthEvent,
|
|
||||||
getListTags,
|
|
||||||
} from "@welshman/util"
|
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
import {Nip01Signer} from "@welshman/signer"
|
import {profileSearch} from "@welshman/app"
|
||||||
import {signer, profileSearch, userBlossomServers} from "@welshman/app"
|
|
||||||
import type {FileAttributes} from "@welshman/editor"
|
import type {FileAttributes} from "@welshman/editor"
|
||||||
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
|
import {Editor, MentionSuggestion, WelshmanExtension} from "@welshman/editor"
|
||||||
import {makeMentionNodeView} from "./MentionNodeView"
|
import {makeMentionNodeView} from "@app/editor/MentionNodeView"
|
||||||
import ProfileSuggestion from "./ProfileSuggestion.svelte"
|
import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||||
|
import {uploadFile} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {compressFile} from "@src/lib/html"
|
|
||||||
|
|
||||||
export const getBlossomServer = () => {
|
|
||||||
const userUrls = getTagValues("server", getListTags(userBlossomServers.get()))
|
|
||||||
|
|
||||||
for (const url of userUrls) {
|
|
||||||
return url.replace(/^ws/, "http")
|
|
||||||
}
|
|
||||||
|
|
||||||
return "https://cdn.satellite.earth"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const makeEditor = async ({
|
export const makeEditor = async ({
|
||||||
aggressive = false,
|
aggressive = false,
|
||||||
@@ -70,59 +51,9 @@ export const makeEditor = async ({
|
|||||||
},
|
},
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
config: {
|
config: {
|
||||||
upload: async (attrs: FileAttributes) => {
|
upload: (attrs: FileAttributes) => uploadFile(attrs.file, {encrypt: true}),
|
||||||
let file: Blob = attrs.file
|
onDrop: () => uploading?.set(true),
|
||||||
|
onComplete: () => uploading?.set(false),
|
||||||
if (!file.type.match("image/(webp|gif)")) {
|
|
||||||
file = await compressFile(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {ciphertext, key, nonce, algorithm} = await encryptFile(file)
|
|
||||||
const tags = [
|
|
||||||
["decryption-key", key],
|
|
||||||
["decryption-nonce", nonce],
|
|
||||||
["encryption-algorithm", algorithm],
|
|
||||||
]
|
|
||||||
|
|
||||||
file = new File([new Blob([ciphertext])], attrs.file.name, {
|
|
||||||
type: "application/octet-stream",
|
|
||||||
})
|
|
||||||
|
|
||||||
const server = getBlossomServer()
|
|
||||||
const hashes = [await sha256(await file.arrayBuffer())]
|
|
||||||
const $signer = signer.get() || Nip01Signer.ephemeral()
|
|
||||||
const authTemplate = makeBlossomAuthEvent({action: "upload", server, hashes})
|
|
||||||
const authEvent = await $signer.sign(authTemplate)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await uploadBlob(server, file, {authEvent})
|
|
||||||
const text = await res.text()
|
|
||||||
|
|
||||||
let {uploaded, url, ...task} = parseJson(text) || {}
|
|
||||||
|
|
||||||
if (!uploaded) {
|
|
||||||
return {error: text}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always append file extension if missing
|
|
||||||
if (new URL(url).pathname.split(".").length === 1) {
|
|
||||||
url += "." + attrs.file.type.split("/")[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = {...task, tags, url}
|
|
||||||
|
|
||||||
return {result}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error(e)
|
|
||||||
return {error: e.toString()}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDrop() {
|
|
||||||
uploading?.set(true)
|
|
||||||
},
|
|
||||||
onComplete() {
|
|
||||||
uploading?.set(false)
|
|
||||||
},
|
|
||||||
onUploadError(currentEditor, task) {
|
onUploadError(currentEditor, task) {
|
||||||
currentEditor.commands.removeFailedUploads()
|
currentEditor.commands.removeFailedUploads()
|
||||||
pushToast({theme: "error", message: task.error})
|
pushToast({theme: "error", message: task.error})
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import {randomId} from "@welshman/lib"
|
|
||||||
import {preventDefault, stopPropagation, compressFile} from "@lib/html"
|
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
|
||||||
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
|
|
||||||
import GallerySend from "@assets/icons/gallery-send.svg?dataurl"
|
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
file?: File | undefined
|
|
||||||
url?: string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
let {file = $bindable(undefined), url = $bindable(undefined)}: Props = $props()
|
|
||||||
|
|
||||||
const id = randomId()
|
|
||||||
|
|
||||||
const onDragEnter = () => {
|
|
||||||
active = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDragOver = () => {
|
|
||||||
active = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDragLeave = () => {
|
|
||||||
active = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDrop = async (e: any) => {
|
|
||||||
active = false
|
|
||||||
|
|
||||||
file = await compressFile(e.dataTransfer.files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
const onChange = async (e: any) => {
|
|
||||||
file = await compressFile(e.target.files[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
const onClear = () => {
|
|
||||||
initialUrl = undefined
|
|
||||||
file = undefined
|
|
||||||
url = undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
let active = $state(false)
|
|
||||||
let initialUrl = $state(url)
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (file) {
|
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
reader.addEventListener(
|
|
||||||
"load",
|
|
||||||
() => {
|
|
||||||
url = reader.result as string
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
} else {
|
|
||||||
url = initialUrl
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<form>
|
|
||||||
<input {id} type="file" accept="image/*" onchange={onChange} class="hidden" />
|
|
||||||
<label
|
|
||||||
for={id}
|
|
||||||
aria-label="Drag and drop files here."
|
|
||||||
style="background-image: url({url});"
|
|
||||||
class="relative flex h-24 w-24 shrink-0 cursor-pointer items-center justify-center rounded-full border-2 border-dashed border-base-content bg-base-300 bg-cover bg-center transition-all"
|
|
||||||
class:border-primary={active}
|
|
||||||
ondragenter={stopPropagation(preventDefault(onDragEnter))}
|
|
||||||
ondragover={stopPropagation(preventDefault(onDragOver))}
|
|
||||||
ondragleave={stopPropagation(preventDefault(onDragLeave))}
|
|
||||||
ondrop={stopPropagation(preventDefault(onDrop))}>
|
|
||||||
<div
|
|
||||||
class="absolute right-0 top-0 h-5 w-5 overflow-hidden rounded-full bg-primary"
|
|
||||||
class:bg-error={file}
|
|
||||||
class:bg-primary={!file}>
|
|
||||||
{#if file}
|
|
||||||
<span
|
|
||||||
role="button"
|
|
||||||
tabindex="-1"
|
|
||||||
onmousedown={stopPropagation(onClear)}
|
|
||||||
ontouchstart={stopPropagation(onClear)}>
|
|
||||||
<Icon icon={CloseCircle} class="scale-150 !bg-base-300" />
|
|
||||||
</span>
|
|
||||||
{:else}
|
|
||||||
<Icon icon={AddCircle} class="scale-150 !bg-base-300" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if !file}
|
|
||||||
<Icon icon={GallerySend} size={7} />
|
|
||||||
{/if}
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
Reference in New Issue
Block a user