+216
-18
Diff
round #0
+56
-3
src/components/PostControls/PostMenu/PostMenuItems.tsx
+56
-3
src/components/PostControls/PostMenu/PostMenuItems.tsx
···
17
17
type AppBskyFeedThreadgate,
18
18
AtUri,
19
19
type RichText as RichTextAPI,
20
+
type BlobRef,
20
21
} from '@atproto/api'
21
22
import {msg} from '@lingui/macro'
22
23
import {useLingui} from '@lingui/react'
···
230
231
width: number
231
232
height: number
232
233
altText?: string
234
+
blobRef?: AppBskyEmbedImages.Image['image']
233
235
}[] = []
234
236
237
+
const recordEmbed = record.embed
238
+
let recordImages: AppBskyEmbedImages.Image[] = []
239
+
if (recordEmbed?.$type === 'app.bsky.embed.images') {
240
+
recordImages = (recordEmbed as AppBskyEmbedImages.Main).images
241
+
} else if (recordEmbed?.$type === 'app.bsky.embed.recordWithMedia') {
242
+
const media = (recordEmbed as AppBskyEmbedRecordWithMedia.Main).media
243
+
if (media.$type === 'app.bsky.embed.images') {
244
+
recordImages = (media as AppBskyEmbedImages.Main).images
245
+
}
246
+
}
247
+
235
248
if (post.embed?.$type === 'app.bsky.embed.images#view') {
236
249
const embed = post.embed as AppBskyEmbedImages.View
237
-
imageUris = embed.images.map(img => ({
250
+
imageUris = embed.images.map((img, i) => ({
238
251
uri: img.fullsize,
239
252
width: img.aspectRatio?.width ?? 1000,
240
253
height: img.aspectRatio?.height ?? 1000,
241
254
altText: img.alt,
255
+
blobRef: recordImages[i]?.image,
242
256
}))
243
257
} else if (post.embed?.$type === 'app.bsky.embed.recordWithMedia#view') {
244
258
const embed = post.embed as AppBskyEmbedRecordWithMedia.View
245
259
if (embed.media.$type === 'app.bsky.embed.images#view') {
246
260
const images = embed.media as AppBskyEmbedImages.View
247
-
imageUris = images.images.map(img => ({
261
+
imageUris = images.images.map((img, i) => ({
248
262
uri: img.fullsize,
249
263
width: img.aspectRatio?.width ?? 1000,
250
264
height: img.aspectRatio?.height ?? 1000,
251
265
altText: img.alt,
266
+
blobRef: recordImages[i]?.image,
252
267
}))
253
268
}
254
269
}
···
297
312
}
298
313
}
299
314
315
+
let videoUri: {uri: string; width: number; height: number; blobRef?: BlobRef; altText?: string} | undefined
316
+
let recordVideo: AppBskyEmbedVideo.Main | undefined
317
+
318
+
if (recordEmbed?.$type === 'app.bsky.embed.video') {
319
+
recordVideo = recordEmbed as AppBskyEmbedVideo.Main
320
+
} else if (recordEmbed?.$type === 'app.bsky.embed.recordWithMedia') {
321
+
const media = (recordEmbed as AppBskyEmbedRecordWithMedia.Main).media
322
+
if (media.$type === 'app.bsky.embed.video') {
323
+
recordVideo = media as AppBskyEmbedVideo.Main
324
+
}
325
+
}
326
+
327
+
if (post.embed?.$type === 'app.bsky.embed.video#view') {
328
+
const embed = post.embed as AppBskyEmbedVideo.View
329
+
if (recordVideo) {
330
+
videoUri = {
331
+
uri: embed.playlist || '',
332
+
width: embed.aspectRatio?.width ?? 1000,
333
+
height: embed.aspectRatio?.height ?? 1000,
334
+
blobRef: recordVideo.video,
335
+
altText: embed.alt || '',
336
+
}
337
+
}
338
+
} else if (post.embed?.$type === 'app.bsky.embed.recordWithMedia#view') {
339
+
const embed = post.embed as AppBskyEmbedRecordWithMedia.View
340
+
if (embed.media.$type === 'app.bsky.embed.video#view' && recordVideo) {
341
+
const video = embed.media as AppBskyEmbedVideo.View
342
+
videoUri = {
343
+
uri: video.playlist || '',
344
+
width: video.aspectRatio?.width ?? 1000,
345
+
height: video.aspectRatio?.height ?? 1000,
346
+
blobRef: recordVideo.video,
347
+
altText: video.alt || '',
348
+
}
349
+
}
350
+
}
351
+
300
352
openComposer({
301
353
text: record.text,
302
354
imageUris,
355
+
videoUri,
303
356
onPost: () => {
304
357
onDeletePost()
305
358
},
···
606
659
control={redraftPromptControl}
607
660
title={_(msg`Redraft this skeet?`)}
608
661
description={_(
609
-
msg`This will delete the original skeet and open the composer with its content. (WARNING: DOESN'T WORK ON SKEETS WITH MEDIA ALREADY ATTACHED. Probably no threads support either.)`,
662
+
msg`This will delete the original skeet and open the composer with its content.`,
610
663
)}
611
664
onConfirm={onConfirmRedraft}
612
665
confirmButtonCta={_(msg`Redraft`)}
+20
-4
src/lib/api/index.ts
+20
-4
src/lib/api/index.ts
···
324
324
onStateChange?.(t`Uploading images...`)
325
325
const images: AppBskyEmbedImages.Image[] = await Promise.all(
326
326
imagesDraft.map(async (image, i) => {
327
+
if (image.blobRef) {
328
+
logger.debug(`Reusing existing blob for image #${i}`)
329
+
return {
330
+
image: image.blobRef,
331
+
alt: image.alt,
332
+
aspectRatio: {
333
+
width: image.source.width,
334
+
height: image.source.height,
335
+
},
336
+
}
337
+
}
327
338
logger.debug(`Compressing image #${i}`)
328
339
const {path, width, height, mime} = await compressImage(image)
329
340
logger.debug(`Uploading image #${i}`)
···
356
367
}),
357
368
)
358
369
359
-
// lexicon numbers must be floats
360
-
const width = Math.round(videoDraft.asset.width)
361
-
const height = Math.round(videoDraft.asset.height)
370
+
const width = Math.round(
371
+
videoDraft.asset?.width ||
372
+
('redraftDimensions' in videoDraft ? videoDraft.redraftDimensions.width : 1000)
373
+
)
374
+
const height = Math.round(
375
+
videoDraft.asset?.height ||
376
+
('redraftDimensions' in videoDraft ? videoDraft.redraftDimensions.height : 1000)
377
+
)
362
378
363
379
// aspect ratio values must be >0 - better to leave as unset otherwise
364
380
// posting will fail if aspect ratio is set to 0
···
366
382
367
383
if (!aspectRatio) {
368
384
logger.error(
369
-
`Invalid aspect ratio - got { width: ${videoDraft.asset.width}, height: ${videoDraft.asset.height} }`,
385
+
`Invalid aspect ratio - got { width: ${width}, height: ${height} }`,
370
386
)
371
387
}
372
388
+5
-2
src/state/gallery.ts
+5
-2
src/state/gallery.ts
···
1
+
import {type BlobRef} from '@atproto/api'
1
2
import {
2
3
cacheDirectory,
3
4
deleteAsync,
···
37
38
type ComposerImageBase = {
38
39
alt: string
39
40
source: ImageSource
41
+
blobRef?: BlobRef
40
42
}
41
43
type ComposerImageWithoutTransformation = ComposerImageBase & {
42
44
transformed?: undefined
···
81
83
width: number
82
84
height: number
83
85
altText?: string
86
+
blobRef?: BlobRef
84
87
}
85
88
86
89
export function createInitialImages(
87
90
uris: InitialImage[] = [],
88
91
): ComposerImageWithoutTransformation[] {
89
-
return uris.map(({uri, width, height, altText = ''}) => {
92
+
return uris.map(({uri, width, height, altText = '', blobRef}) => {
90
93
return {
91
94
alt: altText,
92
95
source: {
···
96
99
height: height,
97
100
mime: 'image/jpeg',
98
101
},
102
+
blobRef,
99
103
}
100
104
})
101
105
}
···
197
201
198
202
export async function compressImage(img: ComposerImage): Promise<PickerImage> {
199
203
const source = img.transformed || img.source
200
-
201
204
const [w, h] = containImageRes(source.width, source.height, POST_IMG_MAX)
202
205
203
206
let minQualityPercentage = 0
+3
-2
src/state/shell/composer/index.tsx
+3
-2
src/state/shell/composer/index.tsx
···
3
3
type AppBskyActorDefs,
4
4
type AppBskyFeedDefs,
5
5
type AppBskyUnspeccedGetPostThreadV2,
6
+
type BlobRef,
6
7
type ModerationDecision,
7
8
} from '@atproto/api'
8
9
import {msg} from '@lingui/macro'
···
41
42
mention?: string // handle of user to mention
42
43
openEmojiPicker?: (pos: EmojiPickerPosition | undefined) => void
43
44
text?: string
44
-
imageUris?: {uri: string; width: number; height: number; altText?: string}[]
45
-
videoUri?: {uri: string; width: number; height: number}
45
+
imageUris?: {uri: string; width: number; height: number; altText?: string; blobRef?: BlobRef}[]
46
+
videoUri?: {uri: string; width: number; height: number; blobRef?: BlobRef; altText?: string}
46
47
}
47
48
48
49
type StateContext = ComposerOpts | undefined
+18
-5
src/view/com/composer/Composer.tsx
+18
-5
src/view/com/composer/Composer.tsx
···
119
119
import {SubtitleDialogBtn} from '#/view/com/composer/videos/SubtitleDialog'
120
120
import {VideoPreview} from '#/view/com/composer/videos/VideoPreview'
121
121
import {VideoTranscodeProgress} from '#/view/com/composer/videos/VideoTranscodeProgress'
122
+
import {VideoEmbedRedraft} from '#/view/com/composer/videos/VideoEmbedRedraft'
122
123
import {Text} from '#/view/com/util/text/Text'
123
124
import {UserAvatar} from '#/view/com/util/UserAvatar'
124
125
import {atoms as a, native, useTheme, web} from '#/alf'
···
126
127
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfoIcon} from '#/components/icons/CircleInfo'
127
128
import {EmojiArc_Stroke2_Corner0_Rounded as EmojiSmileIcon} from '#/components/icons/Emoji'
128
129
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
130
+
import {Play_Stroke2_Corner0_Rounded as PlayIcon} from '#/components/icons/Play'
129
131
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
130
132
import {LazyQuoteEmbed} from '#/components/Post/Embed/LazyQuoteEmbed'
131
133
import * as Prompt from '#/components/Prompt'
···
238
240
239
241
const [composerState, composerDispatch] = useReducer(
240
242
composerReducer,
241
-
{
243
+
createComposerState({
242
244
initImageUris,
243
245
initQuoteUri: initQuote?.uri,
244
246
initText,
245
247
initMention,
246
248
initInteractionSettings: preferences?.postInteractionSettings,
247
-
},
248
-
createComposerState,
249
+
initVideoUri,
250
+
}),
249
251
)
250
252
251
253
const thread = composerState.thread
···
297
299
)
298
300
299
301
const onInitVideo = useNonReactiveCallback(() => {
300
-
if (initVideoUri) {
302
+
if (initVideoUri && !initVideoUri.blobRef) {
301
303
selectVideo(activePost.id, initVideoUri)
302
304
}
303
305
})
···
1172
1174
canRemoveQuote: boolean
1173
1175
isActivePost: boolean
1174
1176
}) {
1177
+
const theme = useTheme()
1175
1178
const video = embed.media?.type === 'video' ? embed.media.video : null
1176
1179
return (
1177
1180
<>
···
1226
1229
clear={clearVideo}
1227
1230
/>
1228
1231
) : null)}
1232
+
{!video.asset && video.status === 'done' && 'playlistUri' in video && (
1233
+
<View style={[a.relative, a.mt_lg]}>
1234
+
<VideoEmbedRedraft
1235
+
blobRef={video.pendingPublish?.blobRef!}
1236
+
playlistUri={video.playlistUri}
1237
+
aspectRatio={video.redraftDimensions}
1238
+
onRemove={clearVideo}
1239
+
/>
1240
+
</View>
1241
+
)}
1229
1242
<SubtitleDialogBtn
1230
1243
defaultAltText={video.altText}
1231
1244
saveAltText={altText =>
···
1239
1252
})
1240
1253
}
1241
1254
captions={video.captions}
1242
-
setCaptions={updater => {
1255
+
setCaptions={(updater: (captions: any[]) => any[]) => {
1243
1256
dispatch({
1244
1257
type: 'embed_update_video',
1245
1258
videoAction: {
+20
-2
src/view/com/composer/state/composer.ts
+20
-2
src/view/com/composer/state/composer.ts
···
18
18
postUriToRelativePath,
19
19
toBskyAppUrl,
20
20
} from '#/lib/strings/url-helpers'
21
-
import {type ComposerImage, createInitialImages} from '#/state/gallery'
21
+
import {
22
+
type ComposerImage,
23
+
createInitialImages,
24
+
} from '#/state/gallery'
22
25
import {createPostgateRecord} from '#/state/queries/postgate/util'
23
26
import {type Gif} from '#/state/queries/tenor'
24
27
import {threadgateRecordToAllowUISetting} from '#/state/queries/threadgate'
···
30
33
} from '#/view/com/composer/text-input/text-input-util'
31
34
import {
32
35
createVideoState,
36
+
createRedraftVideoState,
37
+
type RedraftState,
33
38
type VideoAction,
34
39
videoReducer,
35
40
type VideoState,
···
491
496
initImageUris,
492
497
initQuoteUri,
493
498
initInteractionSettings,
499
+
initVideoUri,
494
500
}: {
495
501
initText: string | undefined
496
502
initMention: string | undefined
···
499
505
initInteractionSettings:
500
506
| BskyPreferences['postInteractionSettings']
501
507
| undefined
508
+
initVideoUri?: ComposerOpts['videoUri']
502
509
}): ComposerState {
503
-
let media: ImagesMedia | undefined
510
+
let media: ImagesMedia | VideoMedia | undefined
504
511
if (initImageUris?.length) {
505
512
media = {
506
513
type: 'images',
507
514
images: createInitialImages(initImageUris),
508
515
}
516
+
} else if (initVideoUri?.blobRef) {
517
+
media = {
518
+
type: 'video',
519
+
video: createRedraftVideoState({
520
+
blobRef: initVideoUri.blobRef,
521
+
width: initVideoUri.width,
522
+
height: initVideoUri.height,
523
+
altText: initVideoUri.altText || '',
524
+
playlistUri: initVideoUri.uri,
525
+
}),
526
+
}
509
527
}
510
528
let quote: Link | undefined
511
529
if (initQuoteUri) {
+36
src/view/com/composer/state/video.ts
+36
src/view/com/composer/state/video.ts
···
130
130
captions: CaptionsTrack[]
131
131
}
132
132
133
+
export type RedraftState = {
134
+
status: 'done'
135
+
progress: 100
136
+
abortController: AbortController
137
+
asset: null
138
+
video?: undefined
139
+
jobId?: undefined
140
+
pendingPublish: {blobRef: BlobRef}
141
+
altText: string
142
+
captions: CaptionsTrack[]
143
+
redraftDimensions: {width: number; height: number}
144
+
playlistUri: string
145
+
}
146
+
133
147
export type VideoState =
134
148
| ErrorState
135
149
| CompressingState
136
150
| UploadingState
137
151
| ProcessingState
138
152
| DoneState
153
+
| RedraftState
139
154
140
155
export function createVideoState(
141
156
asset: ImagePickerAsset,
···
151
166
}
152
167
}
153
168
169
+
export function createRedraftVideoState(opts: {
170
+
blobRef: BlobRef
171
+
width: number
172
+
height: number
173
+
altText?: string
174
+
playlistUri: string
175
+
}): RedraftState {
176
+
const noopController = new AbortController()
177
+
return {
178
+
status: 'done',
179
+
progress: 100,
180
+
abortController: noopController,
181
+
asset: null,
182
+
pendingPublish: {blobRef: opts.blobRef},
183
+
altText: opts.altText || '',
184
+
captions: [],
185
+
redraftDimensions: {width: opts.width, height: opts.height},
186
+
playlistUri: opts.playlistUri,
187
+
}
188
+
}
189
+
154
190
export function videoReducer(
155
191
state: VideoState,
156
192
action: VideoAction,
+57
src/view/com/composer/videos/VideoEmbedRedraft.tsx
+57
src/view/com/composer/videos/VideoEmbedRedraft.tsx
···
1
+
import React from 'react'
2
+
import {Platform, View} from 'react-native'
3
+
import {type BlobRef} from '@atproto/api'
4
+
import {BlueskyVideoView} from '@haileyok/bluesky-video'
5
+
6
+
import {atoms as a} from '#/alf'
7
+
import {ExternalEmbedRemoveBtn} from '../ExternalEmbedRemoveBtn'
8
+
import {VideoEmbedInnerWeb} from '#/components/Post/Embed/VideoEmbed/VideoEmbedInner/VideoEmbedInnerWeb'
9
+
10
+
interface Props {
11
+
blobRef: BlobRef
12
+
playlistUri: string
13
+
aspectRatio: {width: number; height: number}
14
+
onRemove: () => void
15
+
}
16
+
17
+
export function VideoEmbedRedraft({blobRef, playlistUri, aspectRatio, onRemove}: Props) {
18
+
const cidString = blobRef.ref.toString()
19
+
const aspectRatioValue = aspectRatio.width / aspectRatio.height || 16 / 9
20
+
const thumbnailUrl = playlistUri.replace('playlist.m3u8', 'thumbnail.jpg')
21
+
22
+
const mockEmbed = {
23
+
$type: 'app.bsky.embed.video#view' as const,
24
+
video: blobRef,
25
+
playlist: playlistUri,
26
+
thumbnail: thumbnailUrl,
27
+
aspectRatio,
28
+
alt: '',
29
+
captions: [],
30
+
cid: cidString,
31
+
}
32
+
33
+
return (
34
+
<View style={[a.w_full, a.rounded_sm, {aspectRatio: aspectRatioValue}]}>
35
+
{Platform.OS === 'web' ? (
36
+
<VideoEmbedInnerWeb
37
+
embed={mockEmbed}
38
+
active={false}
39
+
setActive={() => {}}
40
+
onScreen={true}
41
+
lastKnownTime={{current: undefined}}
42
+
/>
43
+
) : (
44
+
<BlueskyVideoView
45
+
url={playlistUri}
46
+
autoplay={false}
47
+
beginMuted={true}
48
+
style={[a.flex_1, a.rounded_sm]}
49
+
/>
50
+
)}
51
+
<ExternalEmbedRemoveBtn
52
+
onRemove={onRemove}
53
+
style={{top: 16, right: 16, position: 'absolute', zIndex: 10}}
54
+
/>
55
+
</View>
56
+
)
57
+
}
History
1 round
0 comments
scanash.com
submitted
#0
1 commit
expand
collapse
5666b867
Fix redrafting for images and video
expand 0 comments
pull request successfully merged