@linktr.ee/messaging-react 1.27.0 → 1.28.0-rc-1776225927

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,4 @@
1
- import {
2
- CircleNotchIcon,
3
- PauseIcon,
4
- PlayIcon,
5
- } from '@phosphor-icons/react'
1
+ import { CircleNotchIcon, PauseIcon, PlayIcon } from '@phosphor-icons/react'
6
2
  import React, { useCallback, useEffect, useRef, useState } from 'react'
7
3
  import ReactPlayer from 'react-player'
8
4
 
@@ -27,6 +23,8 @@ export interface MediaPlayerProps {
27
23
  mimeType: string
28
24
  poster?: string
29
25
  autoPlay?: boolean
26
+ /** Controlled playing state. When provided, syncs to internal play/pause. */
27
+ playing?: boolean
30
28
  loop?: boolean
31
29
  controls?: boolean
32
30
  showProgress?: boolean
@@ -40,6 +38,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
40
38
  mimeType,
41
39
  poster,
42
40
  autoPlay = false,
41
+ playing: playingProp,
43
42
  loop = false,
44
43
  controls = true,
45
44
  showProgress = false,
@@ -48,6 +47,18 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
48
47
  }) => {
49
48
  const sourceType = getSourceType(mimeType)
50
49
  const [playing, setPlaying] = useState(autoPlay)
50
+
51
+ // Sync controlled playing prop to internal state
52
+ const prevPlayingPropRef = useRef(playingProp)
53
+ useEffect(() => {
54
+ if (
55
+ playingProp !== undefined &&
56
+ playingProp !== prevPlayingPropRef.current
57
+ ) {
58
+ prevPlayingPropRef.current = playingProp
59
+ setPlaying(playingProp)
60
+ }
61
+ }, [playingProp])
51
62
  const [played, setPlayed] = useState(0)
52
63
  const [seeking, setSeeking] = useState(false)
53
64
  const [scrubberHovered, setScrubberHovered] = useState(false)
@@ -123,7 +134,9 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
123
134
  if (el && el.duration) el.currentTime = fraction * el.duration
124
135
  }, [])
125
136
 
126
- const handleTrackPointerDown = (e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>) => {
137
+ const handleTrackPointerDown = (
138
+ e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
139
+ ) => {
127
140
  e.stopPropagation()
128
141
  setSeeking(true)
129
142
  const fraction = getFraction(e)
@@ -151,7 +164,9 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
151
164
  }, [seeking, getFraction, seekTo])
152
165
 
153
166
  // Use natural aspect ratio once metadata loads, fall back to 16:9 before then.
154
- const aspectStyle = videoAspect ? { aspectRatio: String(videoAspect) } : undefined
167
+ const aspectStyle = videoAspect
168
+ ? { aspectRatio: String(videoAspect) }
169
+ : undefined
155
170
  const aspectClass = !videoAspect ? ' aspect-video' : ''
156
171
  const scrubberPercent = Math.round(played * 100)
157
172
 
@@ -163,7 +178,10 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
163
178
  style={aspectStyle}
164
179
  onClick={() => {
165
180
  if (manualPlayRequired) return
166
- if (onContainerClick) { onContainerClick(); return }
181
+ if (onContainerClick) {
182
+ onContainerClick()
183
+ return
184
+ }
167
185
  if (controls) setPlaying((p) => !p)
168
186
  }}
169
187
  onKeyDown={(e) => {
@@ -178,11 +196,18 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
178
196
  }}
179
197
  >
180
198
  {poster && (
181
- <img src={poster} alt="" className="absolute inset-0 h-full w-full object-cover" />
199
+ <img
200
+ src={poster}
201
+ alt=""
202
+ className="absolute inset-0 h-full w-full object-cover"
203
+ />
182
204
  )}
183
205
  {!poster && (
184
206
  <div className="absolute inset-0 flex items-center justify-center">
185
- {renderTypeIcon(mimeType, { className: 'size-12 text-black/20', weight: 'regular' })}
207
+ {renderTypeIcon(mimeType, {
208
+ className: 'size-12 text-black/20',
209
+ weight: 'regular',
210
+ })}
186
211
  </div>
187
212
  )}
188
213
  <div className="absolute inset-0">
@@ -216,7 +241,10 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
216
241
 
217
242
  {buffering && !manualPlayRequired && (
218
243
  <div className="absolute inset-0 z-10 flex items-center justify-center">
219
- <CircleNotchIcon className="size-8 animate-spin text-white/80" weight="bold" />
244
+ <CircleNotchIcon
245
+ className="size-8 animate-spin text-white/80"
246
+ weight="bold"
247
+ />
220
248
  </div>
221
249
  )}
222
250
 
@@ -244,54 +272,86 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
244
272
  )}
245
273
 
246
274
  {showProgress && !controls && (
247
- <div className="absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent pointer-events-none">
248
- <div className="h-1 w-full overflow-hidden rounded-full bg-white/30">
249
- <div className="h-full rounded-full bg-white" style={{ width: `${scrubberPercent}%` }} />
275
+ <div className="absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent">
276
+ <div
277
+ role="slider"
278
+ aria-label="Playback position"
279
+ aria-valuenow={scrubberPercent}
280
+ aria-valuemin={0}
281
+ aria-valuemax={100}
282
+ tabIndex={0}
283
+ ref={trackRef}
284
+ className="relative flex h-4 w-full cursor-pointer items-center"
285
+ onMouseDown={handleTrackPointerDown}
286
+ onTouchStart={handleTrackPointerDown}
287
+ onClick={(e) => e.stopPropagation()}
288
+ onKeyDown={(e) => {
289
+ if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
290
+ if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
291
+ }}
292
+ >
293
+ <div className="w-full overflow-hidden rounded-full bg-white/30 h-1">
294
+ <div
295
+ className="h-full rounded-full bg-white"
296
+ style={{ width: `${scrubberPercent}%` }}
297
+ />
298
+ </div>
250
299
  </div>
251
300
  </div>
252
301
  )}
253
302
 
254
- {controls && <div className="absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6 transition-all duration-200">
255
- <button
256
- type="button"
257
- onClick={(e) => { e.stopPropagation(); setPlaying((p) => !p) }}
258
- className="shrink-0 text-white"
259
- aria-label={playing ? 'Pause' : 'Play'}
260
- >
261
- {playing
262
- ? <PauseIcon className="size-5" weight="fill" />
263
- : <PlayIcon className="size-5 translate-x-px" weight="fill" />
264
- }
265
- </button>
303
+ {controls && (
304
+ <div className="absolute inset-x-0 bottom-0 flex items-center gap-2 bg-gradient-to-t from-black/60 to-transparent px-3 pb-2.5 pt-6 transition-all duration-200">
305
+ <button
306
+ type="button"
307
+ onClick={(e) => {
308
+ e.stopPropagation()
309
+ setPlaying((p) => !p)
310
+ }}
311
+ className="shrink-0 text-white"
312
+ aria-label={playing ? 'Pause' : 'Play'}
313
+ >
314
+ {playing ? (
315
+ <PauseIcon className="size-5" weight="fill" />
316
+ ) : (
317
+ <PlayIcon className="size-5 translate-x-px" weight="fill" />
318
+ )}
319
+ </button>
266
320
 
267
- <div
268
- role="slider"
269
- aria-label="Playback position"
270
- aria-valuenow={scrubberPercent}
271
- aria-valuemin={0}
272
- aria-valuemax={100}
273
- tabIndex={0}
274
- ref={trackRef}
275
- className="relative flex h-4 w-full cursor-pointer items-center"
276
- onMouseDown={handleTrackPointerDown}
277
- onTouchStart={handleTrackPointerDown}
278
- onClick={(e) => e.stopPropagation()}
279
- onMouseEnter={() => setScrubberHovered(true)}
280
- onMouseLeave={() => setScrubberHovered(false)}
281
- onKeyDown={(e) => {
282
- if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
283
- if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
284
- }}
285
- >
286
- <div className={`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${scrubberHovered || seeking ? 'h-1.5' : 'h-1'}`}>
287
- <div className="h-full rounded-full bg-white" style={{ width: `${scrubberPercent}%` }} />
288
- </div>
289
321
  <div
290
- className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
291
- style={{ left: `${scrubberPercent}%` }}
292
- />
322
+ role="slider"
323
+ aria-label="Playback position"
324
+ aria-valuenow={scrubberPercent}
325
+ aria-valuemin={0}
326
+ aria-valuemax={100}
327
+ tabIndex={0}
328
+ ref={trackRef}
329
+ className="relative flex h-4 w-full cursor-pointer items-center"
330
+ onMouseDown={handleTrackPointerDown}
331
+ onTouchStart={handleTrackPointerDown}
332
+ onClick={(e) => e.stopPropagation()}
333
+ onMouseEnter={() => setScrubberHovered(true)}
334
+ onMouseLeave={() => setScrubberHovered(false)}
335
+ onKeyDown={(e) => {
336
+ if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
337
+ if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
338
+ }}
339
+ >
340
+ <div
341
+ className={`w-full overflow-hidden rounded-full bg-white/30 transition-all duration-200 ${scrubberHovered || seeking ? 'h-1.5' : 'h-1'}`}
342
+ >
343
+ <div
344
+ className="h-full rounded-full bg-white"
345
+ style={{ width: `${scrubberPercent}%` }}
346
+ />
347
+ </div>
348
+ <div
349
+ className={`absolute size-3 -translate-x-1/2 rounded-full bg-white shadow transition-[opacity,transform] duration-200 ${scrubberHovered || seeking ? 'scale-100 opacity-100' : 'scale-0 opacity-0'}`}
350
+ style={{ left: `${scrubberPercent}%` }}
351
+ />
352
+ </div>
293
353
  </div>
294
- </div>}
354
+ )}
295
355
  </div>
296
356
  )
297
357
  }