@linktr.ee/messaging-react 4.4.6 → 4.4.7-rc-1788242278
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.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +739 -710
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/CustomMessage/CustomMessage.test.tsx +149 -4
- package/src/components/CustomMessage/index.tsx +29 -0
- package/src/components/MessageAttachment/MessageAttachment.behavior.test.tsx +45 -2
- package/src/components/MessageAttachment/Video/index.tsx +24 -8
- package/src/logging/index.test.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linktr.ee/messaging-react",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.7-rc-1788242278",
|
|
4
4
|
"description": "React messaging components built on messaging-core for web applications",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"chromatic:local": "yarn storybook:build && chromatic"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@linktr.ee/messaging-core": "
|
|
54
|
-
"@linktr.ee/messaging-taxonomy": "
|
|
53
|
+
"@linktr.ee/messaging-core": "2.6.0-rc-1788242278",
|
|
54
|
+
"@linktr.ee/messaging-taxonomy": "0.11.0-rc-1788242278",
|
|
55
55
|
"@phosphor-icons/react": "^2.1.10"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
@@ -54,13 +54,24 @@ vi.mock('../Avatar', () => ({
|
|
|
54
54
|
Avatar: () => null,
|
|
55
55
|
}))
|
|
56
56
|
|
|
57
|
+
vi.mock('../LinkAttachment', () => ({
|
|
58
|
+
default: {
|
|
59
|
+
Sent: ({ title }: { title?: string }) => <span>{title}</span>,
|
|
60
|
+
Received: ({ title }: { title?: string }) => <span>{title}</span>,
|
|
61
|
+
},
|
|
62
|
+
}))
|
|
63
|
+
|
|
57
64
|
vi.mock('./MessageTag', () => ({
|
|
58
65
|
MessageTag: () => null,
|
|
59
66
|
isAttachmentMessage: () => false,
|
|
60
67
|
isChatbotMessage: () => false,
|
|
61
|
-
isMediaAttachmentMessage: (
|
|
68
|
+
isMediaAttachmentMessage: (message: {
|
|
69
|
+
metadata?: { attachment_content_type?: string }
|
|
70
|
+
}) => message.metadata?.attachment_content_type === 'media',
|
|
62
71
|
isPaidMessage: () => false,
|
|
63
|
-
isTextAttachmentMessage: (
|
|
72
|
+
isTextAttachmentMessage: (message: {
|
|
73
|
+
metadata?: { attachment_content_type?: string }
|
|
74
|
+
}) => message.metadata?.attachment_content_type === 'text',
|
|
64
75
|
hasPaidDeliveryTag: () => false,
|
|
65
76
|
}))
|
|
66
77
|
|
|
@@ -70,16 +81,20 @@ vi.mock('./TipMessage', () => ({
|
|
|
70
81
|
}))
|
|
71
82
|
|
|
72
83
|
vi.mock('./LockedAttachment', () => ({
|
|
73
|
-
default: () =>
|
|
84
|
+
default: () => <span>Locked attachment</span>,
|
|
74
85
|
}))
|
|
75
86
|
|
|
76
87
|
vi.mock('./StreamAttachmentMessage', () => ({
|
|
77
88
|
default: () => null,
|
|
78
89
|
buildOrderedAttachmentSegments: () => [],
|
|
90
|
+
linkCardPropsFromStreamAttachment: (attachment: { title?: string }) => ({
|
|
91
|
+
title: attachment.title,
|
|
92
|
+
}),
|
|
79
93
|
}))
|
|
80
94
|
|
|
81
95
|
vi.mock('../MediaMessage', () => ({
|
|
82
|
-
isLinkAttachment: () =>
|
|
96
|
+
isLinkAttachment: (attachment: { type?: string; og_scrape_url?: string }) =>
|
|
97
|
+
attachment.type === 'link' || !!attachment.og_scrape_url?.trim(),
|
|
83
98
|
}))
|
|
84
99
|
|
|
85
100
|
const mockedUseChannelStateContext = vi.mocked(useChannelStateContext)
|
|
@@ -261,4 +276,134 @@ describe('CustomMessage', () => {
|
|
|
261
276
|
|
|
262
277
|
expect(screen.getByText('Message')).toBeInTheDocument()
|
|
263
278
|
})
|
|
279
|
+
|
|
280
|
+
it('renders a link attachment alongside a locked media attachment', () => {
|
|
281
|
+
mockedUseMessageContext.mockReturnValue({
|
|
282
|
+
message: {
|
|
283
|
+
id: 'locked-media-message',
|
|
284
|
+
text: '',
|
|
285
|
+
type: 'regular',
|
|
286
|
+
status: 'received',
|
|
287
|
+
user: { id: 'me', name: 'Me' },
|
|
288
|
+
metadata: {
|
|
289
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
290
|
+
attachment_content_type: 'media',
|
|
291
|
+
},
|
|
292
|
+
attachments: [
|
|
293
|
+
{
|
|
294
|
+
type: 'link',
|
|
295
|
+
og_scrape_url: 'https://reactjs.org/',
|
|
296
|
+
title: 'ReactJS Philippines',
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
isMyMessage: () => true,
|
|
301
|
+
threadList: false,
|
|
302
|
+
editing: false,
|
|
303
|
+
endOfGroup: true,
|
|
304
|
+
firstOfGroup: true,
|
|
305
|
+
groupedByUser: true,
|
|
306
|
+
handleAction: vi.fn(),
|
|
307
|
+
handleOpenThread: vi.fn(),
|
|
308
|
+
handleRetry: vi.fn(),
|
|
309
|
+
highlighted: false,
|
|
310
|
+
renderText: undefined,
|
|
311
|
+
isMessageAIGenerated: undefined,
|
|
312
|
+
} as never)
|
|
313
|
+
|
|
314
|
+
renderWithProviders(
|
|
315
|
+
<OutboundContext.Provider value={{}}>
|
|
316
|
+
<CustomMessage />
|
|
317
|
+
</OutboundContext.Provider>
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
expect(screen.getByText(/reactjs philippines/i)).toBeInTheDocument()
|
|
321
|
+
expect(screen.getByText('Locked attachment')).toBeInTheDocument()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('renders a link attachment alongside a locked text attachment', () => {
|
|
325
|
+
mockedUseMessageContext.mockReturnValue({
|
|
326
|
+
message: {
|
|
327
|
+
id: 'locked-text-message',
|
|
328
|
+
text: '',
|
|
329
|
+
type: 'regular',
|
|
330
|
+
status: 'received',
|
|
331
|
+
user: { id: 'me', name: 'Me' },
|
|
332
|
+
metadata: {
|
|
333
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
334
|
+
attachment_content_type: 'text',
|
|
335
|
+
},
|
|
336
|
+
attachments: [
|
|
337
|
+
{
|
|
338
|
+
type: 'link',
|
|
339
|
+
og_scrape_url: 'https://reactjs.org/',
|
|
340
|
+
title: 'ReactJS Philippines',
|
|
341
|
+
},
|
|
342
|
+
],
|
|
343
|
+
},
|
|
344
|
+
isMyMessage: () => true,
|
|
345
|
+
threadList: false,
|
|
346
|
+
editing: false,
|
|
347
|
+
endOfGroup: true,
|
|
348
|
+
firstOfGroup: true,
|
|
349
|
+
groupedByUser: true,
|
|
350
|
+
handleAction: vi.fn(),
|
|
351
|
+
handleOpenThread: vi.fn(),
|
|
352
|
+
handleRetry: vi.fn(),
|
|
353
|
+
highlighted: false,
|
|
354
|
+
renderText: undefined,
|
|
355
|
+
isMessageAIGenerated: undefined,
|
|
356
|
+
} as never)
|
|
357
|
+
|
|
358
|
+
renderWithProviders(
|
|
359
|
+
<OutboundContext.Provider value={{}}>
|
|
360
|
+
<CustomMessage />
|
|
361
|
+
</OutboundContext.Provider>
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
expect(screen.getByText(/reactjs philippines/i)).toBeInTheDocument()
|
|
365
|
+
expect(screen.getByText('Locked attachment')).toBeInTheDocument()
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('does not render a link card when a locked message has no link attachments', () => {
|
|
369
|
+
mockedUseMessageContext.mockReturnValue({
|
|
370
|
+
message: {
|
|
371
|
+
id: 'locked-media-without-link',
|
|
372
|
+
text: '',
|
|
373
|
+
type: 'regular',
|
|
374
|
+
status: 'received',
|
|
375
|
+
user: { id: 'me', name: 'Me' },
|
|
376
|
+
metadata: {
|
|
377
|
+
custom_type: 'MESSAGE_ATTACHMENT',
|
|
378
|
+
attachment_content_type: 'media',
|
|
379
|
+
},
|
|
380
|
+
attachments: [
|
|
381
|
+
{
|
|
382
|
+
type: 'image',
|
|
383
|
+
image_url: 'https://cdn.example/image.jpg',
|
|
384
|
+
},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
isMyMessage: () => true,
|
|
388
|
+
threadList: false,
|
|
389
|
+
editing: false,
|
|
390
|
+
endOfGroup: true,
|
|
391
|
+
firstOfGroup: true,
|
|
392
|
+
groupedByUser: true,
|
|
393
|
+
handleAction: vi.fn(),
|
|
394
|
+
handleOpenThread: vi.fn(),
|
|
395
|
+
handleRetry: vi.fn(),
|
|
396
|
+
highlighted: false,
|
|
397
|
+
renderText: undefined,
|
|
398
|
+
isMessageAIGenerated: undefined,
|
|
399
|
+
} as never)
|
|
400
|
+
|
|
401
|
+
renderWithProviders(
|
|
402
|
+
<OutboundContext.Provider value={{}}>
|
|
403
|
+
<CustomMessage />
|
|
404
|
+
</OutboundContext.Provider>
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
expect(screen.queryByText(/reactjs philippines/i)).not.toBeInTheDocument()
|
|
408
|
+
})
|
|
264
409
|
})
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import type { OutboundClickHandler, ResolvedOutbound } from '../../types'
|
|
33
33
|
import { getMessageDisplayText } from '../../utils/getMessageDisplayText'
|
|
34
34
|
import { Avatar } from '../Avatar'
|
|
35
|
+
import LinkAttachment from '../LinkAttachment'
|
|
35
36
|
import { isLinkAttachment } from '../MediaMessage'
|
|
36
37
|
import type { BubbleGroupPosition } from '../MessageAttachment/types'
|
|
37
38
|
|
|
@@ -49,6 +50,7 @@ import { OutboundContext } from './OutboundContext'
|
|
|
49
50
|
import { SentMessageDeliveryStatus } from './SentMessageDeliveryStatus'
|
|
50
51
|
import StreamAttachmentMessage, {
|
|
51
52
|
buildOrderedAttachmentSegments,
|
|
53
|
+
linkCardPropsFromStreamAttachment,
|
|
52
54
|
} from './StreamAttachmentMessage'
|
|
53
55
|
import { TipMessage, isTipMessage } from './TipMessage'
|
|
54
56
|
|
|
@@ -123,6 +125,10 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
123
125
|
const filtered = raw.filter((a) => !('type' in a) || !isLinkAttachment(a))
|
|
124
126
|
return filtered.length === raw.length ? raw : filtered
|
|
125
127
|
}, [message])
|
|
128
|
+
const lockedLinkAttachments = useMemo(
|
|
129
|
+
() => (message.attachments ?? []).filter(isLinkAttachment),
|
|
130
|
+
[message]
|
|
131
|
+
)
|
|
126
132
|
const attachmentSegments = useMemo(
|
|
127
133
|
() => buildOrderedAttachmentSegments(finalAttachments),
|
|
128
134
|
[finalAttachments]
|
|
@@ -270,6 +276,27 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
270
276
|
{showTail && <MessageTail side={isMine ? 'me' : 'other'} />}
|
|
271
277
|
</div>
|
|
272
278
|
)
|
|
279
|
+
const lockedLinkCards = lockedLinkAttachments.length > 0 && (
|
|
280
|
+
<div
|
|
281
|
+
className={classNames(
|
|
282
|
+
'flex flex-col gap-[4px]',
|
|
283
|
+
isMine ? 'items-end' : 'items-start'
|
|
284
|
+
)}
|
|
285
|
+
>
|
|
286
|
+
{lockedLinkAttachments.map((attachment, index) => {
|
|
287
|
+
const Card = isMine ? LinkAttachment.Sent : LinkAttachment.Received
|
|
288
|
+
const isFinalRow =
|
|
289
|
+
index === lockedLinkAttachments.length - 1 && !message.text
|
|
290
|
+
return (
|
|
291
|
+
<Card
|
|
292
|
+
key={`locked-link-${index}`}
|
|
293
|
+
{...linkCardPropsFromStreamAttachment(attachment)}
|
|
294
|
+
groupPosition={isFinalRow ? bubbleGroupPosition : 'middle'}
|
|
295
|
+
/>
|
|
296
|
+
)
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
299
|
+
)
|
|
273
300
|
return (
|
|
274
301
|
<>
|
|
275
302
|
{editing && (
|
|
@@ -340,6 +367,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
340
367
|
)}
|
|
341
368
|
{!isMine && MessageActions && <MessageActions />}
|
|
342
369
|
</div>
|
|
370
|
+
{lockedLinkCards}
|
|
343
371
|
</div>
|
|
344
372
|
) : isMediaAttachmentMessage(message) ? (
|
|
345
373
|
<div className="str-chat__message-bubble-wrapper message-locked-attachment-wrapper">
|
|
@@ -374,6 +402,7 @@ const CustomMessageWithContext = (props: CustomMessageWithContextProps) => {
|
|
|
374
402
|
)}
|
|
375
403
|
{!isMine && MessageActions && <MessageActions />}
|
|
376
404
|
</div>
|
|
405
|
+
{lockedLinkCards}
|
|
377
406
|
{message.text && accompanyingTextBubble}
|
|
378
407
|
</div>
|
|
379
408
|
) : canRenderAttachmentsInToolkit ? (
|
|
@@ -105,7 +105,7 @@ describe('MessageAttachment behavior', () => {
|
|
|
105
105
|
)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
it('renders video
|
|
108
|
+
it('renders video posters and posterless first-frame previews', () => {
|
|
109
109
|
const onDismiss = vi.fn()
|
|
110
110
|
const { rerender } = renderWithProviders(
|
|
111
111
|
<MessageAttachment.Video.Composer
|
|
@@ -121,12 +121,55 @@ describe('MessageAttachment behavior', () => {
|
|
|
121
121
|
).toHaveAttribute('src', '/poster.jpg')
|
|
122
122
|
fireEvent.click(screen.getByRole('button', { name: 'Dismiss attachment' }))
|
|
123
123
|
expect(onDismiss).toHaveBeenCalledOnce()
|
|
124
|
+
rerender(
|
|
125
|
+
<MessageAttachment.Video.Sent src="https://cdn.example.com/video.mp4" />
|
|
126
|
+
)
|
|
127
|
+
expect(screen.getByLabelText('Video 1 thumbnail')).toHaveAttribute(
|
|
128
|
+
'src',
|
|
129
|
+
'https://cdn.example.com/video.mp4#t=0.001'
|
|
130
|
+
)
|
|
131
|
+
expect(screen.getByLabelText('Play video 1 of 1')).toBeInTheDocument()
|
|
132
|
+
|
|
133
|
+
rerender(
|
|
134
|
+
<MessageAttachment.Video.Sent src="https://cdn.example.com/video.mp4#track=fr" />
|
|
135
|
+
)
|
|
136
|
+
expect(screen.getByLabelText('Video 1 thumbnail')).toHaveAttribute(
|
|
137
|
+
'src',
|
|
138
|
+
'https://cdn.example.com/video.mp4#track=fr&t=0.001'
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
rerender(
|
|
142
|
+
<MessageAttachment.Video.Sent src="https://cdn.example.com/video.mp4#t=5" />
|
|
143
|
+
)
|
|
144
|
+
expect(screen.getByLabelText('Video 1 thumbnail')).toHaveAttribute(
|
|
145
|
+
'src',
|
|
146
|
+
'https://cdn.example.com/video.mp4#t=5'
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
rerender(
|
|
150
|
+
<MessageAttachment.Video.Received src="https://cdn.example.com/received-video.mp4" />
|
|
151
|
+
)
|
|
152
|
+
expect(screen.getByLabelText('Video 1 thumbnail')).toHaveAttribute(
|
|
153
|
+
'src',
|
|
154
|
+
'https://cdn.example.com/received-video.mp4#t=0.001'
|
|
155
|
+
)
|
|
156
|
+
|
|
124
157
|
rerender(
|
|
125
158
|
<MessageAttachment.Video.Sent
|
|
126
|
-
|
|
159
|
+
src="https://cdn.example.com/video.mp4"
|
|
160
|
+
preload="none"
|
|
127
161
|
/>
|
|
128
162
|
)
|
|
163
|
+
expect(screen.queryByLabelText('Video 1 thumbnail')).not.toBeInTheDocument()
|
|
129
164
|
expect(screen.getByLabelText('Play video 1 of 1')).toBeInTheDocument()
|
|
165
|
+
|
|
166
|
+
rerender(
|
|
167
|
+
<MessageAttachment.Video.Composer
|
|
168
|
+
src="https://cdn.example.com/video.mp4"
|
|
169
|
+
preload="none"
|
|
170
|
+
/>
|
|
171
|
+
)
|
|
172
|
+
expect(screen.queryByLabelText('Video 1 thumbnail')).not.toBeInTheDocument()
|
|
130
173
|
})
|
|
131
174
|
|
|
132
175
|
it('supports PDF titles, download actions, dismiss, and viewer index', () => {
|
|
@@ -42,10 +42,10 @@ export interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
|
42
42
|
items?: VideoItem[]
|
|
43
43
|
/**
|
|
44
44
|
* `<video preload>` hint forwarded into the viewer. Defaults to
|
|
45
|
-
* `'none'` —
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* `VideoItem.preload`. The opened `VideoViewer` always preloads
|
|
45
|
+
* `'none'` — poster images carry the visual weight on the bubble surface,
|
|
46
|
+
* while posterless tiles fetch metadata to paint a first frame unless
|
|
47
|
+
* `'none'` is explicitly set, in which case they use the placeholder icon.
|
|
48
|
+
* Per-item overrides live on `VideoItem.preload`. The opened `VideoViewer` always preloads
|
|
49
49
|
* metadata for the active item (so duration / first-frame appear
|
|
50
50
|
* immediately) regardless of this value.
|
|
51
51
|
*/
|
|
@@ -70,9 +70,8 @@ export interface VideoAttachmentSharedProps extends MessageAttachmentBaseProps {
|
|
|
70
70
|
* recorded it, and only otherwise measured off the decoded poster. Measuring
|
|
71
71
|
* the poster alone was not enough: an uploaded clip may have no poster, and a
|
|
72
72
|
* portrait video then sat in whatever `FALLBACK_ASPECT` guessed. Reading the
|
|
73
|
-
* dimensions Stream already stores costs nothing at render time
|
|
74
|
-
*
|
|
75
|
-
* thread against the deliberate `preload: 'none'`.
|
|
73
|
+
* dimensions Stream already stores costs nothing at render time; posterless
|
|
74
|
+
* tiles fetch metadata to paint a first frame, without using it for sizing.
|
|
76
75
|
*
|
|
77
76
|
* `FALLBACK_ASPECT` is **square** on purpose. It covers only the case where we
|
|
78
77
|
* know nothing — no recorded dimensions, no decoded poster — and a square box
|
|
@@ -101,6 +100,14 @@ const PlayBadge: React.FC = () => (
|
|
|
101
100
|
</div>
|
|
102
101
|
)
|
|
103
102
|
|
|
103
|
+
// Safari needs a time fragment to paint a first frame from a remote MP4.
|
|
104
|
+
const withFirstFrameHint = (src: string): string => {
|
|
105
|
+
const hashIndex = src.indexOf('#')
|
|
106
|
+
if (hashIndex === -1) return `${src}#t=0.001`
|
|
107
|
+
const fragment = src.slice(hashIndex + 1)
|
|
108
|
+
return /(^|&)t=/.test(fragment) ? src : `${src}&t=0.001`
|
|
109
|
+
}
|
|
110
|
+
|
|
104
111
|
// The tile shell owns the cluster's geometry: `MediaStackGrid` gives
|
|
105
112
|
// each tile the 18px radius on the cluster's OUTER corners only and
|
|
106
113
|
// clips with `overflow-hidden`, so a poster must carry no radius of
|
|
@@ -137,6 +144,15 @@ const PosterTile: React.FC<{
|
|
|
137
144
|
onLoad={onLoad}
|
|
138
145
|
className="absolute inset-0 size-full object-cover"
|
|
139
146
|
/>
|
|
147
|
+
) : item.src && item.preload !== 'none' ? (
|
|
148
|
+
<video
|
|
149
|
+
src={withFirstFrameHint(item.src)}
|
|
150
|
+
muted
|
|
151
|
+
playsInline
|
|
152
|
+
preload="metadata"
|
|
153
|
+
aria-label={`Video ${index + 1} thumbnail`}
|
|
154
|
+
className="absolute inset-0 size-full object-cover"
|
|
155
|
+
/>
|
|
140
156
|
) : (
|
|
141
157
|
<div className="absolute inset-0 flex items-center justify-center">
|
|
142
158
|
<VideoCameraIcon
|
|
@@ -264,7 +280,7 @@ const VideoComposerInner: React.FC<{
|
|
|
264
280
|
aria-label="Play video"
|
|
265
281
|
className="relative block size-[280px] cursor-pointer overflow-hidden rounded-md outline-none focus-visible:ring-2 focus-visible:ring-black/40"
|
|
266
282
|
>
|
|
267
|
-
<PosterTile item={{ src, poster, mimeType }} index={0} />
|
|
283
|
+
<PosterTile item={{ src, poster, mimeType, preload }} index={0} />
|
|
268
284
|
</button>
|
|
269
285
|
{onDismiss ? (
|
|
270
286
|
<div className="absolute right-2 top-2 z-10">
|
|
@@ -22,7 +22,7 @@ const ErrorEmitter: React.FC<{ error: Error }> = ({ error }) => {
|
|
|
22
22
|
/** Emits one line at every level so partial-sink fallback is observable. */
|
|
23
23
|
const AllLevelsEmitter: React.FC = () => {
|
|
24
24
|
const logger = useMessagingLogger()
|
|
25
|
-
//
|
|
25
|
+
// This is the messaging logger's debug level, not screen.debug().
|
|
26
26
|
logger.debug('debug line', 1)
|
|
27
27
|
logger.info('info line', 2)
|
|
28
28
|
logger.warn('warn line', 3)
|