diff --git a/CHANGELOG.md b/CHANGELOG.md index 60f8da9..c7f08a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## Unreleased + +### Added + +* `ImmersClient.sendImage` and `ImmersClient.sendVideo` - new high level apis to upload and share media + +### Changed + +* `Activities.image` and `Activities.video` now accept Blob/File and upload them with postMedia in addition to urls + ## v2.8.2 (2022-09-28) ### Fixed diff --git a/package.json b/package.json index 8311de9..e33c4f1 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,10 @@ "webpack-merge": "^5.8.0" }, "standard": { - "parser": "@babel/eslint-parser" + "parser": "@babel/eslint-parser", + "env": { + "browser": true + } }, "dependencies": { "core-js": "^3.17.2", diff --git a/source/activities.js b/source/activities.js index 6898c59..4edae24 100644 --- a/source/activities.js +++ b/source/activities.js @@ -128,15 +128,17 @@ export class Activities { * Post an activity with media upload * @param {Activities.APActivity} activity * @param {Blob} file - * @param {Blob} icon + * @param {Blob} [icon] */ async postMedia (activity, file, icon) { if (!this.trustedIRI(this.actor.endpoints.uploadMedia)) { throw new Error('Missing/invalid upload media endpoint') } const formData = new globalThis.FormData() - formData.append('file', file, 'NiceFreeTreasure.glb') - formData.append('icon', icon, 'NiceFreeTreasure.png') + formData.append('file', file, file.name ?? 'UploadedFile') + if (icon) { + formData.append('icon', icon, icon.name ?? 'UploadedIcon') + } formData.append('object', JSON.stringify(activity)) const result = await window.fetch(this.actor.endpoints.uploadMedia, { method: 'POST', @@ -311,9 +313,8 @@ export class Activities { return this.getObject(friendsEndpoint) } - image (url, to, audience, summary) { + image (urlOrBlob, to, audience, summary) { const obj = { - url, type: 'Image', attributedTo: this.actor.id, context: this.place, @@ -328,7 +329,14 @@ export class Activities { if (audience === 'public') { obj.to.push(Activities.PublicAddress) } - return this.postActivity(obj) + if (typeof urlOrBlob === 'string') { + obj.url = urlOrBlob + return this.postActivity(obj) + } + if (urlOrBlob instanceof Blob) { + return this.postMedia(obj, urlOrBlob) + } + return Promise.reject(new Error('Image must be either url string or Blob data')) } note (content, to, audience, summary) { @@ -397,9 +405,8 @@ export class Activities { return this.postActivity(activity) } - video (url, to, audience, summary) { + video (urlOrBlob, to, audience, summary) { const obj = { - url, type: 'Video', attributedTo: this.actor.id, context: this.place, @@ -414,6 +421,13 @@ export class Activities { if (audience === 'public') { obj.to.push(Activities.PublicAddress) } - return this.postActivity(obj) + if (typeof urlOrBlob === 'string') { + obj.url = urlOrBlob + return this.postActivity(obj) + } + if (urlOrBlob instanceof Blob) { + return this.postMedia(obj, urlOrBlob) + } + return Promise.reject(new Error('Image must be either url string or Blob data')) } } diff --git a/source/client.js b/source/client.js index 6814e1e..7689133 100644 --- a/source/client.js +++ b/source/client.js @@ -410,6 +410,53 @@ export class ImmersClient extends window.EventTarget { return this.activities.note(DOMPurify.sanitize(content), to, privacy) } + /** + * Upload and/or share an image. + * When image is a canvas element, its toBlob method is used to generate a + * png image to upload. + * When image is a File/Blob, it will be uploaded to the user's home immer + * and shared. The `name` attribute is optional, but `type` must contain the + * correct MIME. When image is a url, an existing image is shared without + * re-uploading. It's better to upload a file so that the user's home + * immer can ensure it remains available. + * privacy level determines who receives and can acccess the message. + * direct: Only those named in `to` receive the message. + * friends: Direct plus friends list. + * public: Direct plus Friends plus accessible via URL for sharing. + * @param {(File|Blob|HTMLCanvasElement|string)} image - Image data to upload or url to share + * @param {string} privacy - 'direct', 'friends', or 'public' + * @param {string[]} [to] - Addressees. Accepts Immers handles (username[domain.name]) and ActivityPub IRIs + * @returns {Promise} Url of newly posted message + */ + async sendImage (image, privacy, to = []) { + if (image instanceof HTMLCanvasElement) { + image = await new Promise(resolve => { + image.toBlob(resolve) + }) + } + return this.activities.image(image, to, privacy) + } + + /** + * Upload and/or share a video. + * When video is a File/Blob, it will be uploaded to the user's home immer + * and shared. The `name` attribute is optional, but `type` must contain the + * correct MIME. When video is a url, an existing video is shared without + * re-uploading. It's better to upload a file so that the user's home + * immer can ensure it remains available. + * privacy level determines who receives and can acccess the message. + * direct: Only those named in `to` receive the message. + * friends: Direct plus friends list. + * public: Direct plus Friends plus accessible via URL for sharing. + * @param {(File|Blob|string)} video - Video data to upload or url to share + * @param {string} privacy - 'direct', 'friends', or 'public' + * @param {string[]} [to] - Addressees. Accepts Immers handles (username[domain.name]) and ActivityPub IRIs + * @returns {Promise} Url of newly posted message + */ + sendVideo (video, privacy, to = []) { + return this.activities.video(video, to, privacy) + } + /** * This method will either initiate a new friend request or, * if a request has already been received from the target user,