@linktr.ee/messaging-react 1.33.3 → 1.34.0-rc-1777521550

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.
package/dist/index.d.ts CHANGED
@@ -233,8 +233,10 @@ export declare interface ChannelViewProps {
233
233
  export declare interface CreatorCardProps extends LockedAttachmentBaseProps {
234
234
  placeholderTitle?: string;
235
235
  placeholderAmountText?: string;
236
+ isUnlocking?: boolean;
236
237
  onDismiss?: () => void;
237
- onPreviewClick?: () => LockedAttachmentSource;
238
+ onPreviewClick?: () => void;
239
+ onFetchSource?: () => Promise<LockedAttachmentSource | void>;
238
240
  }
239
241
 
240
242
  export declare const CustomMessageProvider: Provider<Partial<CustomMessageRegistry>>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as u, f as M, h as c, i as l, j as h, P as C, k as P, u as d, l as L, m as p, n as v } from "./index-Ydi1pTAi.js";
1
+ import { A as e, a as t, C as i, b as n, c as o, d as g, F as r, e as m, L as u, f as M, h as c, i as l, j as h, P as C, k as P, u as d, l as L, m as p, n as v } from "./index-DWk0f1PF.js";
2
2
  export {
3
3
  e as ActionButton,
4
4
  t as Avatar,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@linktr.ee/messaging-react",
3
- "version": "1.33.3",
3
+ "version": "1.34.0-rc-1777521550",
4
4
  "description": "React messaging components built on messaging-core for web applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -208,7 +208,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
208
208
  }}
209
209
  >
210
210
  {isAttachment ? (
211
- <div className={`flex flex-col gap-1 ${isMine ? 'items-end' : 'items-start'}`}>
211
+ <div className="str-chat__message-bubble-wrapper">
212
212
  {isMine ? (
213
213
  <LockedAttachment.Creator
214
214
  title={message.metadata?.attachment_title}
@@ -217,6 +217,11 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
217
217
  amountText={message.metadata?.amount_text}
218
218
  detail={message.metadata?.attachment_detail}
219
219
  paymentStatus={message.metadata?.payment_status}
220
+ isUnlocking={isUnlocking(message.id)}
221
+ onPreviewClick={() => onUnlockClick?.(message, channel)}
222
+ onFetchSource={async () =>
223
+ await onFetchSource?.(message, channel)
224
+ }
220
225
  />
221
226
  ) : (
222
227
  <LockedAttachment.Visitor
@@ -235,10 +240,8 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
235
240
  />
236
241
  )}
237
242
  {message.text && (
238
- <div className="str-chat__message-bubble-wrapper">
239
- <div className="str-chat__message-bubble">
240
- <MessageText message={message} renderText={renderText} />
241
- </div>
243
+ <div className="str-chat__message-bubble">
244
+ <MessageText message={message} renderText={renderText} />
242
245
  </div>
243
246
  )}
244
247
  </div>
@@ -21,7 +21,7 @@ const AUDIO_SOURCE = '/audio-source.mp3'
21
21
 
22
22
  const meta: Meta = {
23
23
  title: 'Components/LockedAttachment',
24
- parameters: { layout: 'centered' },
24
+ parameters: { layout: 'fullscreen' },
25
25
  }
26
26
  export default meta
27
27
 
@@ -62,10 +62,19 @@ const VARIANTS = [
62
62
  thumbnailUnlockedUrl: DOCUMENT_THUMBNAIL,
63
63
  sourceUrl: DOCUMENT_SOURCE,
64
64
  },
65
+ {
66
+ label: 'Unknown',
67
+ title: 'Unknown Attachment',
68
+ mimeType: 'application/octet-stream',
69
+ detail: '2.4 MB',
70
+ thumbnailUrl: undefined,
71
+ thumbnailUnlockedUrl: undefined,
72
+ sourceUrl: DOCUMENT_SOURCE,
73
+ },
65
74
  ]
66
75
 
67
76
  const Table = ({ children }: { children: React.ReactNode }) => (
68
- <div className="p-12">
77
+ <div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
69
78
  <table className="border-separate border-spacing-4">{children}</table>
70
79
  </div>
71
80
  )
@@ -90,8 +99,9 @@ const TableHead = ({
90
99
  </thead>
91
100
  )
92
101
 
93
- export const Visitor: StoryFn = () => {
102
+ export const Received: StoryFn = () => {
94
103
  const [isPaid, setPaid] = useState<string | undefined>()
104
+ const [isUnlocking, setUnlocking] = useState<string | undefined>()
95
105
 
96
106
  return (
97
107
  <Table>
@@ -118,14 +128,16 @@ export const Visitor: StoryFn = () => {
118
128
  detail={detail}
119
129
  paymentStatus={isPaid === mimeType ? 'paid' : undefined}
120
130
  amountText="AU$9.99"
121
- onUnlockClick={() => setPaid(mimeType)}
122
- onDownloadClick={() => alert('Download clicked')}
123
- onFetchSource={async () => {
124
- return Promise.resolve({
125
- sourceUrl: sourceUrl,
126
- thumbnailUrl: thumbnailUnlockedUrl,
127
- })
131
+ isUnlocking={isUnlocking === mimeType}
132
+ onUnlockClick={() => {
133
+ setUnlocking(mimeType)
134
+ setTimeout(() => {
135
+ setUnlocking(undefined)
136
+ setPaid(mimeType)
137
+ }, 1500)
128
138
  }}
139
+ onDownloadClick={() => alert('Download clicked')}
140
+ onFetchSource={async () => ({ sourceUrl: sourceUrl, thumbnailUrl: thumbnailUnlockedUrl })}
129
141
  />
130
142
  </td>
131
143
  )
@@ -133,9 +145,9 @@ export const Visitor: StoryFn = () => {
133
145
  </tr>
134
146
  <tr>
135
147
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
136
- Purchased
148
+ Unlocked
137
149
  </td>
138
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
150
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, thumbnailUnlockedUrl, sourceUrl }) => (
139
151
  <td key={mimeType} className="align-top">
140
152
  <LockedAttachment.Visitor
141
153
  title={title}
@@ -146,51 +158,18 @@ export const Visitor: StoryFn = () => {
146
158
  paymentStatus="paid"
147
159
  onUnlockClick={() => alert('Unlock clicked')}
148
160
  onDownloadClick={() => alert('Download clicked')}
161
+ onFetchSource={async () => ({ sourceUrl: sourceUrl, thumbnailUrl: thumbnailUnlockedUrl })}
149
162
  />
150
163
  </td>
151
164
  ))}
152
165
  </tr>
153
- <tr>
154
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
155
- Unlocked
156
- </td>
157
- {VARIANTS.map(
158
- ({
159
- title,
160
- mimeType,
161
- detail,
162
- thumbnailUrl,
163
- thumbnailUnlockedUrl,
164
- sourceUrl,
165
- }) => (
166
- <td key={mimeType} className="align-top">
167
- <LockedAttachment.Visitor
168
- title={title}
169
- thumbnailUrl={thumbnailUrl}
170
- mimeType={mimeType}
171
- detail={detail}
172
- amountText="AU$9.99"
173
- paymentStatus="paid"
174
- onUnlockClick={() => console.log('Unlock clicked')}
175
- onDownloadClick={() => alert('Download clicked')}
176
- onFetchSource={async () => {
177
- await new Promise((resolve) => setTimeout(resolve, 500))
178
- return Promise.resolve({
179
- sourceUrl: sourceUrl,
180
- thumbnailUrl: thumbnailUnlockedUrl,
181
- })
182
- }}
183
- />
184
- </td>
185
- )
186
- )}
187
- </tr>
166
+
188
167
  </tbody>
189
168
  </Table>
190
169
  )
191
170
  }
192
171
 
193
- export const Creator: StoryFn = () => (
172
+ export const Sent: StoryFn = () => (
194
173
  <Table>
195
174
  <TableHead variants={VARIANTS} />
196
175
  <tbody>
@@ -213,10 +192,10 @@ export const Creator: StoryFn = () => (
213
192
  thumbnailUrl={thumbnailUrl}
214
193
  mimeType={mimeType}
215
194
  detail={detail}
216
- onPreviewClick={() => ({
217
- sourceUrl: sourceUrl,
218
- thumbnailUrl: thumbnailUnlockedUrl,
219
- })}
195
+ onFetchSource={async () => {
196
+ await new Promise((resolve) => setTimeout(resolve, 1500))
197
+ return { sourceUrl: sourceUrl, thumbnailUrl: thumbnailUnlockedUrl }
198
+ }}
220
199
  />
221
200
  </td>
222
201
  )
@@ -243,7 +222,7 @@ export const Creator: StoryFn = () => (
243
222
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
244
223
  Sent
245
224
  </td>
246
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
225
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, thumbnailUnlockedUrl, sourceUrl }) => (
247
226
  <td key={mimeType} className="align-top">
248
227
  <LockedAttachment.Creator
249
228
  title={title}
@@ -251,6 +230,10 @@ export const Creator: StoryFn = () => (
251
230
  mimeType={mimeType}
252
231
  detail={detail}
253
232
  amountText="AU$9.99"
233
+ onFetchSource={async () => {
234
+ await new Promise((resolve) => setTimeout(resolve, 1500))
235
+ return { sourceUrl: sourceUrl, thumbnailUrl: thumbnailUnlockedUrl }
236
+ }}
254
237
  />
255
238
  </td>
256
239
  ))}
@@ -259,7 +242,7 @@ export const Creator: StoryFn = () => (
259
242
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
260
243
  Sold
261
244
  </td>
262
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
245
+ {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl, thumbnailUnlockedUrl, sourceUrl }) => (
263
246
  <td key={mimeType} className="align-top">
264
247
  <LockedAttachment.Creator
265
248
  title={title}
@@ -268,6 +251,10 @@ export const Creator: StoryFn = () => (
268
251
  detail={detail}
269
252
  amountText="AU$9.99"
270
253
  paymentStatus="paid"
254
+ onFetchSource={async () => {
255
+ await new Promise((resolve) => setTimeout(resolve, 1500))
256
+ return { sourceUrl: sourceUrl, thumbnailUrl: thumbnailUnlockedUrl }
257
+ }}
271
258
  />
272
259
  </td>
273
260
  ))}
@@ -7,7 +7,7 @@ import {
7
7
  XIcon,
8
8
  } from '@phosphor-icons/react'
9
9
  import classNames from 'classnames'
10
- import React, { useCallback, useState } from 'react'
10
+ import React, { useCallback, useRef, useState } from 'react'
11
11
 
12
12
  import type {
13
13
  LockedAttachmentBaseProps,
@@ -21,8 +21,10 @@ import CardThumbnail from './CardThumbnail'
21
21
  export interface CreatorCardProps extends LockedAttachmentBaseProps {
22
22
  placeholderTitle?: string
23
23
  placeholderAmountText?: string
24
+ isUnlocking?: boolean
24
25
  onDismiss?: () => void
25
- onPreviewClick?: () => LockedAttachmentSource
26
+ onPreviewClick?: () => void
27
+ onFetchSource?: () => Promise<LockedAttachmentSource | void>
26
28
  }
27
29
 
28
30
  const CreatorCard: React.FC<CreatorCardProps> = ({
@@ -34,29 +36,56 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
34
36
  placeholderTitle = 'Attachment title',
35
37
  placeholderAmountText,
36
38
  paymentStatus,
39
+ isUnlocking,
37
40
  onDismiss,
38
41
  onPreviewClick,
42
+ onFetchSource,
39
43
  }) => {
40
44
  const [source, setSource] = useState<LockedAttachmentSource | undefined>()
41
-
42
- const effectiveSourceUrl = source?.sourceUrl
43
- const effectiveThumbnailUrl = source?.thumbnailUrl ?? thumbnailUrl
44
-
45
- const handleToggle = useCallback(() => {
45
+ const [isPreviewVisible, setIsPreviewVisible] = useState(false)
46
+ const [isLoadingPreview, setLoadingPreview] = useState(false)
47
+ const fetchingRef = useRef(false)
48
+
49
+ const effectiveSourceUrl = isPreviewVisible ? source?.sourceUrl : undefined
50
+ const effectiveThumbnailUrl = isPreviewVisible ? (source?.thumbnailUrl ?? thumbnailUrl) : thumbnailUrl
51
+ const isBusy = isLoadingPreview || isUnlocking
52
+
53
+ const handleToggle = useCallback(async () => {
54
+ onPreviewClick?.()
55
+ if (isPreviewVisible) {
56
+ setIsPreviewVisible(false)
57
+ return
58
+ }
46
59
  if (source) {
47
- setSource(undefined)
48
- } else if (onPreviewClick) {
49
- setSource(onPreviewClick())
60
+ setIsPreviewVisible(true)
61
+ return
50
62
  }
51
- }, [source, onPreviewClick])
63
+ if (!onFetchSource) return
64
+ if (fetchingRef.current) return
65
+ fetchingRef.current = true
66
+ setLoadingPreview(true)
67
+ try {
68
+ const result = await onFetchSource()
69
+ if (result) {
70
+ setSource(result)
71
+ setIsPreviewVisible(true)
72
+ }
73
+ } finally {
74
+ fetchingRef.current = false
75
+ setLoadingPreview(false)
76
+ }
77
+ }, [isPreviewVisible, source, onPreviewClick, onFetchSource])
78
+
79
+ const toggleHandler = onFetchSource || onPreviewClick ? handleToggle : undefined
52
80
 
53
81
  return (
54
- <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
82
+ <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-[#121110] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
55
83
  <CardHeader
56
84
  onDismiss={onDismiss}
57
- onToggle={onPreviewClick ? handleToggle : undefined}
58
- isExpanded={!!source}
85
+ onToggle={isBusy ? undefined : toggleHandler}
86
+ isExpanded={isPreviewVisible}
59
87
  paymentStatus={paymentStatus}
88
+ isLoading={isBusy}
60
89
  />
61
90
 
62
91
  <CardThumbnail
@@ -64,12 +93,12 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
64
93
  sourceUrl={effectiveSourceUrl}
65
94
  thumbnailUrl={effectiveThumbnailUrl}
66
95
  mimeType={mimeType}
67
- onToggle={onPreviewClick ? handleToggle : undefined}
96
+ onToggle={isBusy ? undefined : toggleHandler}
68
97
  />
69
98
 
70
- <div className="px-4 pb-3 pt-3 bg-black">
99
+ <div className="px-4 pb-3 pt-3">
71
100
  <p
72
- className={classNames('mb-1.5 truncate text-base font-medium', {
101
+ className={classNames('mb-0.5 truncate text-base font-medium', {
73
102
  'text-white/30': !title,
74
103
  'text-white': !!title,
75
104
  })}
@@ -90,11 +119,8 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
90
119
  {paymentStatus === 'paid' ? (
91
120
  <React.Fragment>
92
121
  <span className="text-xs font-medium text-white/55">&bull;</span>
93
- <span className="text-xs font-medium text-[#4ade80]">Sold</span>
94
- <CheckCircleIcon
95
- className="size-4 text-[#4ade80]"
96
- weight="bold"
97
- />
122
+ <span className="text-xs font-medium text-[#34c759]">Sold</span>
123
+ <CheckCircleIcon className="size-4 text-[#34c759]" weight="bold" />
98
124
  </React.Fragment>
99
125
  ) : (
100
126
  <React.Fragment>
@@ -120,6 +146,7 @@ interface CardHeaderProps {
120
146
  onToggle?: () => void
121
147
  isExpanded?: boolean
122
148
  paymentStatus?: PaymentStatus
149
+ isLoading?: boolean
123
150
  }
124
151
 
125
152
  const CardHeader: React.FC<CardHeaderProps> = ({
@@ -127,6 +154,7 @@ const CardHeader: React.FC<CardHeaderProps> = ({
127
154
  onToggle,
128
155
  isExpanded,
129
156
  paymentStatus,
157
+ isLoading,
130
158
  }) => {
131
159
  if (onDismiss) {
132
160
  return (
@@ -160,7 +188,11 @@ const CardHeader: React.FC<CardHeaderProps> = ({
160
188
 
161
189
  return (
162
190
  <div className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3">
163
- <Icon className="size-4" weight="fill" />
191
+ {isLoading ? (
192
+ <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
193
+ ) : (
194
+ <Icon className="size-4" weight="fill" />
195
+ )}
164
196
  </div>
165
197
  )
166
198
  }
@@ -27,7 +27,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
27
27
  type="button"
28
28
  disabled={!onToggle}
29
29
  className={classNames(
30
- 'relative block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',
30
+ 'relative block w-full overflow-hidden border-0 bg-white/10 p-0 text-left appearance-none',
31
31
  { 'cursor-pointer': !!onToggle, 'cursor-default': !onToggle }
32
32
  )}
33
33
  onClick={onToggle}
@@ -51,7 +51,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
51
51
  ) : (
52
52
  <div className="aspect-video flex items-center justify-center">
53
53
  {renderTypeIcon(mimeType, {
54
- className: 'size-12 text-black/20',
54
+ className: 'size-12 text-white/20',
55
55
  weight: 'regular',
56
56
  })}
57
57
  </div>
@@ -111,37 +111,30 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
111
111
  paymentStatus={paymentStatus}
112
112
  />
113
113
 
114
- <div className="px-4 pb-3 pt-3 bg-black">
115
- <p className="mb-1.5 truncate text-base font-medium text-white">
114
+ <div className="px-4 pb-3 pt-3">
115
+ <p className="mb-0.5 truncate text-base font-medium text-black">
116
116
  {title}
117
117
  </p>
118
118
  <div className="flex items-center gap-1">
119
119
  {renderTypeIcon(mimeType, {
120
- className: 'size-5 shrink-0 text-white/55',
120
+ className: 'size-5 shrink-0 text-black/55',
121
121
  weight: 'regular',
122
122
  })}
123
123
 
124
124
  {detail && (
125
- <span className="text-xs font-medium text-white/55">{detail}</span>
125
+ <span className="text-xs font-medium text-black/55">{detail}</span>
126
126
  )}
127
127
 
128
128
  {paymentStatus === 'paid' ? (
129
129
  <React.Fragment>
130
- <span className="text-xs font-medium text-white/55">&bull;</span>
131
- <span className="text-xs font-medium text-[#4ade80]">
132
- Purchased
133
- </span>
134
- <CheckCircleIcon
135
- className="size-4 text-[#4ade80]"
136
- weight="bold"
137
- />
130
+ <span className="text-xs font-medium text-black/55">&bull;</span>
131
+ <span className="text-xs font-medium text-[#008236]">Purchased</span>
132
+ <CheckCircleIcon className="size-4 text-[#008236]" weight="bold" />
138
133
  </React.Fragment>
139
134
  ) : amountText != null ? (
140
135
  <React.Fragment>
141
- <span className="text-xs font-medium text-white/55">&bull;</span>
142
- <span className="text-xs font-medium text-white/55">
143
- {amountText}
144
- </span>
136
+ <span className="text-xs font-medium text-black/55">&bull;</span>
137
+ <span className="text-xs font-medium text-black/55">{amountText}</span>
145
138
  </React.Fragment>
146
139
  ) : null}
147
140
  </div>
@@ -26,10 +26,10 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
26
26
  type="button"
27
27
  onClick={onUnlockClicked}
28
28
  disabled={isUnlocking}
29
- className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none text-[#121110] hover:bg-white/90 disabled:opacity-70"
29
+ className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70"
30
30
  >
31
31
  {isUnlocking ? (
32
- <LoadingDots />
32
+ <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
33
33
  ) : (
34
34
  <React.Fragment>
35
35
  <LockSimpleIcon className="size-4" weight="fill" />
@@ -47,7 +47,7 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
47
47
  target="_blank"
48
48
  rel="noopener noreferrer"
49
49
  onClick={onDownloadClicked}
50
- className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none !text-[#121110] hover:bg-white/90"
50
+ className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none !text-white hover:bg-[#2a2928]"
51
51
  >
52
52
  <DownloadSimpleIcon className="size-4" weight="bold" />
53
53
  Download
@@ -58,14 +58,5 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
58
58
  return null
59
59
  }
60
60
 
61
- const LoadingDots: React.FC = () => {
62
- return (
63
- <span className="flex items-center gap-1">
64
- <span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.3s]" />
65
- <span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.15s]" />
66
- <span className="size-1 rounded-full bg-white animate-bounce" />
67
- </span>
68
- )
69
- }
70
61
 
71
62
  export default CardActions
@@ -38,11 +38,11 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
38
38
  }
39
39
 
40
40
  return (
41
- <div className="relative overflow-hidden bg-black/5">
41
+ <div className="relative aspect-video overflow-hidden bg-black/5">
42
42
  <img
43
43
  src={sourceType === 'document' ? thumbnailUrl : sourceUrl}
44
44
  alt={title}
45
- className={`block w-full transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
45
+ className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
46
46
  draggable={false}
47
47
  onLoad={() => setSourceReady(true)}
48
48
  />