@planningcenter/chat-react-native 4.3.0 → 4.3.1-qa-989.0
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/build/components/conversation/attachments/giphy_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/giphy_attachment.js +2 -2
- package/build/components/conversation/attachments/giphy_attachment.js.map +1 -1
- package/build/components/conversation/attachments/image_attachment.d.ts.map +1 -1
- package/build/components/conversation/attachments/image_attachment.js +5 -3
- package/build/components/conversation/attachments/image_attachment.js.map +1 -1
- package/build/components/conversation/message.d.ts +3 -1
- package/build/components/conversation/message.d.ts.map +1 -1
- package/build/components/conversation/message.js +183 -158
- package/build/components/conversation/message.js.map +1 -1
- package/build/components/conversation/message_form.d.ts.map +1 -1
- package/build/components/conversation/message_form.js +2 -2
- package/build/components/conversation/message_form.js.map +1 -1
- package/build/components/conversation/message_list.d.ts.map +1 -1
- package/build/components/conversation/message_list.js +143 -88
- package/build/components/conversation/message_list.js.map +1 -1
- package/build/components/conversation/system_message.js +3 -3
- package/build/components/conversation/system_message.js.map +1 -1
- package/build/components/conversations/conversations.d.ts.map +1 -1
- package/build/components/conversations/conversations.js +0 -6
- package/build/components/conversations/conversations.js.map +1 -1
- package/build/contexts/api_provider.d.ts.map +1 -1
- package/build/contexts/api_provider.js +28 -25
- package/build/contexts/api_provider.js.map +1 -1
- package/build/hooks/use_animated_message_background_color.d.ts +1 -0
- package/build/hooks/use_animated_message_background_color.d.ts.map +1 -1
- package/build/hooks/use_animated_message_background_color.js +26 -9
- package/build/hooks/use_animated_message_background_color.js.map +1 -1
- package/build/hooks/use_chat_permissions.d.ts +10 -2
- package/build/hooks/use_chat_permissions.d.ts.map +1 -1
- package/build/hooks/use_chat_permissions.js +8 -0
- package/build/hooks/use_chat_permissions.js.map +1 -1
- package/build/hooks/use_features.d.ts +2 -0
- package/build/hooks/use_features.d.ts.map +1 -1
- package/build/hooks/use_features.js +3 -1
- package/build/hooks/use_features.js.map +1 -1
- package/build/hooks/use_jolt.d.ts.map +1 -1
- package/build/hooks/use_jolt.js +2 -1
- package/build/hooks/use_jolt.js.map +1 -1
- package/build/hooks/use_product_analytics.d.ts +9 -0
- package/build/hooks/use_product_analytics.d.ts.map +1 -1
- package/build/hooks/use_product_analytics.js +4 -2
- package/build/hooks/use_product_analytics.js.map +1 -1
- package/build/hooks/use_search_enabled.d.ts +2 -0
- package/build/hooks/use_search_enabled.d.ts.map +1 -0
- package/build/hooks/use_search_enabled.js +18 -0
- package/build/hooks/use_search_enabled.js.map +1 -0
- package/build/screens/age_check/age_check_underage_screen.d.ts.map +1 -1
- package/build/screens/age_check/age_check_underage_screen.js +2 -1
- package/build/screens/age_check/age_check_underage_screen.js.map +1 -1
- package/build/screens/attachment_actions/attachment_actions_screen.d.ts.map +1 -1
- package/build/screens/attachment_actions/attachment_actions_screen.js +5 -2
- package/build/screens/attachment_actions/attachment_actions_screen.js.map +1 -1
- package/build/screens/bug_report_screen.d.ts.map +1 -1
- package/build/screens/bug_report_screen.js +2 -2
- package/build/screens/bug_report_screen.js.map +1 -1
- package/build/screens/conversations/components/search_list_header_component.js +3 -3
- package/build/screens/conversations/components/search_list_header_component.js.map +1 -1
- package/build/screens/conversations/conversations_screen.d.ts.map +1 -1
- package/build/screens/conversations/conversations_screen.js +2 -5
- package/build/screens/conversations/conversations_screen.js.map +1 -1
- package/build/screens/get_help_screen.js +2 -2
- package/build/screens/get_help_screen.js.map +1 -1
- package/build/screens/message_actions_screen.js +6 -2
- package/build/screens/message_actions_screen.js.map +1 -1
- package/build/screens/search/components/conversation_results.d.ts +1 -1
- package/build/screens/search/components/conversation_results.d.ts.map +1 -1
- package/build/screens/search/components/conversation_results.js +2 -2
- package/build/screens/search/components/conversation_results.js.map +1 -1
- package/build/screens/search/components/search_results_list.d.ts +7 -2
- package/build/screens/search/components/search_results_list.d.ts.map +1 -1
- package/build/screens/search/components/search_results_list.js +135 -105
- package/build/screens/search/components/search_results_list.js.map +1 -1
- package/build/screens/search/search_screen.d.ts.map +1 -1
- package/build/screens/search/search_screen.js +88 -25
- package/build/screens/search/search_screen.js.map +1 -1
- package/build/utils/native_adapters/linking.d.ts +11 -3
- package/build/utils/native_adapters/linking.d.ts.map +1 -1
- package/build/utils/native_adapters/linking.js.map +1 -1
- package/build/utils/response_error.d.ts +1 -1
- package/build/utils/response_error.d.ts.map +1 -1
- package/build/utils/response_error.js +1 -1
- package/build/utils/response_error.js.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/components/conversation/attachments/giphy_attachment.test.tsx +76 -0
- package/src/__tests__/contexts/api_provider.test.tsx +50 -0
- package/src/__tests__/screens/attachment_actions_screen.test.tsx +81 -0
- package/src/__tests__/screens/get_help_screen.test.ts +36 -0
- package/src/__tests__/screens/message_actions_screen.test.tsx +136 -0
- package/src/components/conversation/__tests__/message_list.test.tsx +71 -3
- package/src/components/conversation/attachments/giphy_attachment.tsx +2 -3
- package/src/components/conversation/attachments/image_attachment.tsx +2 -3
- package/src/components/conversation/message.tsx +12 -1
- package/src/components/conversation/message_form.tsx +1 -1
- package/src/components/conversation/message_list.tsx +26 -1
- package/src/components/conversation/system_message.tsx +3 -3
- package/src/components/conversations/conversations.tsx +0 -7
- package/src/contexts/__tests__/api_provider.test.tsx +106 -0
- package/src/contexts/api_provider.tsx +6 -9
- package/src/hooks/__tests__/use_jolt.test.tsx +70 -0
- package/src/hooks/use_animated_message_background_color.ts +37 -12
- package/src/hooks/use_chat_permissions.ts +15 -2
- package/src/hooks/use_features.ts +2 -0
- package/src/hooks/use_jolt.ts +10 -7
- package/src/hooks/use_product_analytics.ts +3 -1
- package/src/hooks/use_search_enabled.ts +9 -0
- package/src/screens/age_check/age_check_underage_screen.tsx +2 -1
- package/src/screens/attachment_actions/attachment_actions_screen.tsx +3 -2
- package/src/screens/bug_report_screen.tsx +2 -10
- package/src/screens/conversations/__tests__/conversations_screen.test.tsx +71 -0
- package/src/screens/conversations/components/search_list_header_component.tsx +1 -1
- package/src/screens/conversations/conversations_screen.tsx +2 -3
- package/src/screens/get_help_screen.tsx +2 -2
- package/src/screens/message_actions_screen.tsx +6 -2
- package/src/screens/search/__tests__/search_screen.test.tsx +130 -0
- package/src/screens/search/components/__tests__/conversation_results.test.tsx +2 -2
- package/src/screens/search/components/__tests__/search_results_list.test.tsx +47 -2
- package/src/screens/search/components/conversation_results.tsx +7 -3
- package/src/screens/search/components/search_results_list.tsx +25 -4
- package/src/screens/search/search_screen.tsx +44 -6
- package/src/utils/__tests__/response_error.test.ts +7 -2
- package/src/utils/native_adapters/__tests__/linking.test.ts +12 -0
- package/src/utils/native_adapters/linking.ts +12 -3
- package/src/utils/response_error.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response_error.js","names":["ResponseError","Error","constructor","response","status","statusText","name","errors","isResponseLike","value","candidate","ok","url","headers","clone","json","text","arrayBuffer","throwResponseError","error"
|
|
1
|
+
{"version":3,"file":"response_error.js","names":["ResponseError","Error","constructor","response","status","statusText","name","errors","isResponseLike","value","candidate","ok","url","headers","clone","json","text","arrayBuffer","throwResponseError","error"],"sources":["../../src/utils/response_error.ts"],"sourcesContent":["import { FailedResponse } from '../types/api_primitives'\n\nexport class ResponseError extends Error {\n status: number\n statusText: string\n errors: FailedResponse['errors']\n response: FailedResponse\n\n constructor(response: FailedResponse) {\n super(`ResponseError: ${response?.status} ${response?.statusText}`)\n this.name = 'ResponseError'\n this.status = response?.status\n this.statusText = response?.statusText\n this.errors = response?.errors || []\n this.response = response\n }\n}\n\n// Detect a fetch Response by shape, not `instanceof`: Expo's winter runtime\n// swaps in a `fetch` whose `FetchResponse` isn't an instance of the global\n// `Response`, so `instanceof` silently misses it under Expo. We avoid checking\n// `redirected` because React Native's whatwg-fetch polyfill never assigns it,\n// which would reject every real response under RN's fetch.\nexport const isResponseLike = (value: unknown): value is Response => {\n if (typeof value !== 'object' || value === null) return false\n\n const candidate = value as Record<string, unknown>\n return (\n typeof candidate.status === 'number' &&\n typeof candidate.statusText === 'string' &&\n typeof candidate.ok === 'boolean' &&\n typeof candidate.url === 'string' &&\n typeof candidate.headers === 'object' &&\n candidate.headers !== null &&\n typeof candidate.clone === 'function' &&\n typeof candidate.json === 'function' &&\n typeof candidate.text === 'function' &&\n typeof candidate.arrayBuffer === 'function'\n )\n}\n\nexport const throwResponseError = (error: unknown): never => {\n if (isResponseLike(error)) {\n throw new ResponseError(error as FailedResponse)\n }\n\n throw error\n}\n"],"mappings":"AAEA,OAAO,MAAMA,aAAa,SAASC,KAAK,CAAC;EAMvCC,WAAWA,CAACC,QAAwB,EAAE;IACpC,KAAK,CAAC,kBAAkBA,QAAQ,EAAEC,MAAM,IAAID,QAAQ,EAAEE,UAAU,EAAE,CAAC;IACnE,IAAI,CAACC,IAAI,GAAG,eAAe;IAC3B,IAAI,CAACF,MAAM,GAAGD,QAAQ,EAAEC,MAAM;IAC9B,IAAI,CAACC,UAAU,GAAGF,QAAQ,EAAEE,UAAU;IACtC,IAAI,CAACE,MAAM,GAAGJ,QAAQ,EAAEI,MAAM,IAAI,EAAE;IACpC,IAAI,CAACJ,QAAQ,GAAGA,QAAQ;EAC1B;AACF;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMK,cAAc,GAAIC,KAAc,IAAwB;EACnE,IAAI,OAAOA,KAAK,KAAK,QAAQ,IAAIA,KAAK,KAAK,IAAI,EAAE,OAAO,KAAK;EAE7D,MAAMC,SAAS,GAAGD,KAAgC;EAClD,OACE,OAAOC,SAAS,CAACN,MAAM,KAAK,QAAQ,IACpC,OAAOM,SAAS,CAACL,UAAU,KAAK,QAAQ,IACxC,OAAOK,SAAS,CAACC,EAAE,KAAK,SAAS,IACjC,OAAOD,SAAS,CAACE,GAAG,KAAK,QAAQ,IACjC,OAAOF,SAAS,CAACG,OAAO,KAAK,QAAQ,IACrCH,SAAS,CAACG,OAAO,KAAK,IAAI,IAC1B,OAAOH,SAAS,CAACI,KAAK,KAAK,UAAU,IACrC,OAAOJ,SAAS,CAACK,IAAI,KAAK,UAAU,IACpC,OAAOL,SAAS,CAACM,IAAI,KAAK,UAAU,IACpC,OAAON,SAAS,CAACO,WAAW,KAAK,UAAU;AAE/C,CAAC;AAED,OAAO,MAAMC,kBAAkB,GAAIC,KAAc,IAAY;EAC3D,IAAIX,cAAc,CAACW,KAAK,CAAC,EAAE;IACzB,MAAM,IAAInB,aAAa,CAACmB,KAAuB,CAAC;EAClD;EAEA,MAAMA,KAAK;AACb,CAAC","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "4.3.0",
|
|
3
|
+
"version": "4.3.1-qa-989.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@fortawesome/fontawesome-svg-core": "^7.2.0",
|
|
35
35
|
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
|
36
|
-
"@planningcenter/emoji-keyboard": "4.3.0",
|
|
36
|
+
"@planningcenter/emoji-keyboard": "4.3.1-qa-989.0",
|
|
37
37
|
"lodash-inflection": "^1.5.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"sf-symbols-typescript": "^2.2.0",
|
|
85
85
|
"typescript": "~6.0.3"
|
|
86
86
|
},
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "bd271384ad50f354963f1a563d59bbd616caa42e"
|
|
88
88
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
3
|
+
import React from 'react'
|
|
4
|
+
import { Linking as RNLinking } from 'react-native'
|
|
5
|
+
import { GiphyAttachment } from '../../../../components/conversation/attachments/giphy_attachment'
|
|
6
|
+
import {
|
|
7
|
+
DenormalizedGiphyAttachmentResource,
|
|
8
|
+
GiphyAttachmentVariant,
|
|
9
|
+
} from '../../../../types/resources/denormalized_attachment_resource'
|
|
10
|
+
import {
|
|
11
|
+
AudioAdapter,
|
|
12
|
+
ChatAdapters,
|
|
13
|
+
ClipboardAdapter,
|
|
14
|
+
ImagePickerAdapter,
|
|
15
|
+
LinkingAdapter,
|
|
16
|
+
VideoAdapter,
|
|
17
|
+
} from '../../../../utils/native_adapters'
|
|
18
|
+
|
|
19
|
+
const variant: GiphyAttachmentVariant = {
|
|
20
|
+
url: 'https://media.giphy.com/cat.gif',
|
|
21
|
+
width: '200',
|
|
22
|
+
height: '100',
|
|
23
|
+
size: '1',
|
|
24
|
+
frames: '1',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const attachment: DenormalizedGiphyAttachmentResource = {
|
|
28
|
+
type: 'giphy',
|
|
29
|
+
id: '1',
|
|
30
|
+
title: 'cat',
|
|
31
|
+
titleLink: 'https://giphy.com/gifs/cat',
|
|
32
|
+
thumbUrl: variant.url,
|
|
33
|
+
giphy: {
|
|
34
|
+
original: variant,
|
|
35
|
+
fixedHeight: variant,
|
|
36
|
+
fixedHeightStill: variant,
|
|
37
|
+
fixedHeightDownsampled: variant,
|
|
38
|
+
fixedWidth: variant,
|
|
39
|
+
fixedWidthStill: variant,
|
|
40
|
+
fixedWidthDownsampled: variant,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const hostOpenURL = jest.fn(async (_: string) => {})
|
|
45
|
+
|
|
46
|
+
describe('GiphyAttachment', () => {
|
|
47
|
+
beforeAll(() => {
|
|
48
|
+
ChatAdapters.configure({
|
|
49
|
+
clipboard: new ClipboardAdapter({
|
|
50
|
+
getStringAsync: async () => '',
|
|
51
|
+
setStringAsync: async () => {},
|
|
52
|
+
}),
|
|
53
|
+
audio: new AudioAdapter({ useAudio: () => ({}) as any }),
|
|
54
|
+
video: new VideoAdapter({ Player: React.forwardRef(() => null) }),
|
|
55
|
+
imagePicker: new ImagePickerAdapter({
|
|
56
|
+
openCameraAsync: async () => ({ canceled: true, assets: null }),
|
|
57
|
+
openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
|
|
58
|
+
}),
|
|
59
|
+
linking: new LinkingAdapter({ openURL: hostOpenURL, canOpenURL: async () => true }),
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("opens the Giphy link through the host app's linking adapter", () => {
|
|
64
|
+
const rnOpenURL = jest.spyOn(RNLinking, 'openURL')
|
|
65
|
+
const { getByText } = render(
|
|
66
|
+
<ThemeProvider value={DefaultTheme}>
|
|
67
|
+
<GiphyAttachment attachment={attachment} onMessageLongPress={jest.fn()} />
|
|
68
|
+
</ThemeProvider>
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
fireEvent.press(getByText('/giphy'))
|
|
72
|
+
|
|
73
|
+
expect(hostOpenURL).toHaveBeenCalledWith('https://giphy.com/gifs/cat')
|
|
74
|
+
expect(rnOpenURL).not.toHaveBeenCalled()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { render } from '@testing-library/react-native'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
import { ApiProvider, chatQueryClient } from '../../contexts/api_provider'
|
|
4
|
+
import { ChatContext, ChatContextValue } from '../../contexts/chat_context'
|
|
5
|
+
import { ApiClient, useApiClient } from '../../hooks/use_api_client'
|
|
6
|
+
import { Session } from '../../utils'
|
|
7
|
+
|
|
8
|
+
jest.mock('../../hooks/use_api_client')
|
|
9
|
+
|
|
10
|
+
const mockedUseApiClient = useApiClient as jest.MockedFunction<typeof useApiClient>
|
|
11
|
+
|
|
12
|
+
let get: jest.Mock
|
|
13
|
+
|
|
14
|
+
const renderApiProvider = ({ token }: { token?: { access_token: string } } = {}) => {
|
|
15
|
+
const session = new Session({ token })
|
|
16
|
+
|
|
17
|
+
return render(
|
|
18
|
+
<ChatContext.Provider value={{ session, token } as unknown as ChatContextValue}>
|
|
19
|
+
<ApiProvider>{null}</ApiProvider>
|
|
20
|
+
</ChatContext.Provider>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.useFakeTimers()
|
|
26
|
+
chatQueryClient.clear()
|
|
27
|
+
get = jest.fn().mockResolvedValue({ data: [] })
|
|
28
|
+
mockedUseApiClient.mockReturnValue({ chat: { get } } as unknown as ApiClient)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
chatQueryClient.clear()
|
|
33
|
+
jest.clearAllMocks()
|
|
34
|
+
jest.useRealTimers()
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('ApiProvider', () => {
|
|
38
|
+
it('requests app grants on the first attempt without waiting for a retry', () => {
|
|
39
|
+
renderApiProvider({ token: { access_token: 'a-real-token' } })
|
|
40
|
+
|
|
41
|
+
expect(get).toHaveBeenCalledWith(expect.objectContaining({ url: '/me/app_grants' }))
|
|
42
|
+
expect(get.mock.calls.filter(([args]) => args.url === '/me/app_grants')).toHaveLength(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not prefetch anything without a token', () => {
|
|
46
|
+
renderApiProvider()
|
|
47
|
+
|
|
48
|
+
expect(get).not.toHaveBeenCalled()
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { HeaderHeightContext } from '@react-navigation/elements'
|
|
2
|
+
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
|
3
|
+
import { fireEvent, render } from '@testing-library/react-native'
|
|
4
|
+
import React from 'react'
|
|
5
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
6
|
+
import {
|
|
7
|
+
AttachmentActionsScreen,
|
|
8
|
+
AttachmentActionsScreenProps,
|
|
9
|
+
} from '../../screens/attachment_actions/attachment_actions_screen'
|
|
10
|
+
import {
|
|
11
|
+
AudioAdapter,
|
|
12
|
+
ChatAdapters,
|
|
13
|
+
ClipboardAdapter,
|
|
14
|
+
ImagePickerAdapter,
|
|
15
|
+
LinkingAdapter,
|
|
16
|
+
VideoAdapter,
|
|
17
|
+
} from '../../utils/native_adapters'
|
|
18
|
+
|
|
19
|
+
jest.mock('@react-navigation/native', () => ({
|
|
20
|
+
...jest.requireActual('@react-navigation/native'),
|
|
21
|
+
useNavigation: () => ({ goBack: jest.fn() }),
|
|
22
|
+
}))
|
|
23
|
+
jest.mock('../../screens/attachment_actions/hooks/useDeleteAttachment', () => ({
|
|
24
|
+
useDeleteAttachment: () => ({ handleDeleteAttachment: jest.fn() }),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
const hostOpenURL = jest.fn(async (_url: string, _options?: object) => {})
|
|
28
|
+
|
|
29
|
+
const safeAreaMetrics = {
|
|
30
|
+
frame: { x: 0, y: 0, width: 390, height: 844 },
|
|
31
|
+
insets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const route = {
|
|
35
|
+
key: 'AttachmentActions',
|
|
36
|
+
name: 'AttachmentActions',
|
|
37
|
+
params: {
|
|
38
|
+
attachmentId: '1',
|
|
39
|
+
attachmentContentType: 'application/pdf',
|
|
40
|
+
attachmentUrl: 'https://example.com/file.pdf',
|
|
41
|
+
conversation_id: 1,
|
|
42
|
+
canDeleteNonAuthoredMessages: false,
|
|
43
|
+
myMessage: false,
|
|
44
|
+
},
|
|
45
|
+
} as AttachmentActionsScreenProps['route']
|
|
46
|
+
|
|
47
|
+
describe('AttachmentActionsScreen', () => {
|
|
48
|
+
beforeAll(() => {
|
|
49
|
+
ChatAdapters.configure({
|
|
50
|
+
clipboard: new ClipboardAdapter({
|
|
51
|
+
getStringAsync: async () => '',
|
|
52
|
+
setStringAsync: async () => {},
|
|
53
|
+
}),
|
|
54
|
+
audio: new AudioAdapter({ useAudio: () => ({}) as any }),
|
|
55
|
+
video: new VideoAdapter({ Player: React.forwardRef(() => null) }),
|
|
56
|
+
imagePicker: new ImagePickerAdapter({
|
|
57
|
+
openCameraAsync: async () => ({ canceled: true, assets: null }),
|
|
58
|
+
openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
|
|
59
|
+
}),
|
|
60
|
+
linking: new LinkingAdapter({ openURL: hostOpenURL, canOpenURL: async () => true }),
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('asks the host app to open the attachment in the browser', () => {
|
|
65
|
+
const { getByText } = render(
|
|
66
|
+
<SafeAreaProvider initialMetrics={safeAreaMetrics}>
|
|
67
|
+
<ThemeProvider value={DefaultTheme}>
|
|
68
|
+
<HeaderHeightContext.Provider value={0}>
|
|
69
|
+
<AttachmentActionsScreen route={route} />
|
|
70
|
+
</HeaderHeightContext.Provider>
|
|
71
|
+
</ThemeProvider>
|
|
72
|
+
</SafeAreaProvider>
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
fireEvent.press(getByText('Open in browser'))
|
|
76
|
+
|
|
77
|
+
expect(hostOpenURL).toHaveBeenCalledWith('https://example.com/file.pdf', {
|
|
78
|
+
forceOpenInBrowser: true,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { sendEmail } from '../../screens/get_help_screen'
|
|
3
|
+
import {
|
|
4
|
+
AudioAdapter,
|
|
5
|
+
ChatAdapters,
|
|
6
|
+
ClipboardAdapter,
|
|
7
|
+
ImagePickerAdapter,
|
|
8
|
+
LinkingAdapter,
|
|
9
|
+
VideoAdapter,
|
|
10
|
+
} from '../../utils/native_adapters'
|
|
11
|
+
|
|
12
|
+
const hostOpenURL = jest.fn(async (_: string) => {})
|
|
13
|
+
|
|
14
|
+
describe('sendEmail', () => {
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
ChatAdapters.configure({
|
|
17
|
+
clipboard: new ClipboardAdapter({
|
|
18
|
+
getStringAsync: async () => '',
|
|
19
|
+
setStringAsync: async () => {},
|
|
20
|
+
}),
|
|
21
|
+
audio: new AudioAdapter({ useAudio: () => ({}) as any }),
|
|
22
|
+
video: new VideoAdapter({ Player: React.forwardRef(() => null) }),
|
|
23
|
+
imagePicker: new ImagePickerAdapter({
|
|
24
|
+
openCameraAsync: async () => ({ canceled: true, assets: null }),
|
|
25
|
+
openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
|
|
26
|
+
}),
|
|
27
|
+
linking: new LinkingAdapter({ openURL: hostOpenURL, canOpenURL: async () => true }),
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("opens mailto through the host app's linking adapter", () => {
|
|
32
|
+
sendEmail('help@example.com')
|
|
33
|
+
|
|
34
|
+
expect(hostOpenURL).toHaveBeenCalledWith('mailto:help@example.com')
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { HeaderHeightContext } from '@react-navigation/elements'
|
|
2
|
+
import { DefaultTheme, ThemeProvider } from '@react-navigation/native'
|
|
3
|
+
import { QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { fireEvent, render, waitFor } from '@testing-library/react-native'
|
|
5
|
+
import React from 'react'
|
|
6
|
+
import { SafeAreaProvider } from 'react-native-safe-area-context'
|
|
7
|
+
import { buildTestQueryClient } from '../../__utils__/query_client'
|
|
8
|
+
import {
|
|
9
|
+
MessageActionsScreen,
|
|
10
|
+
MessageActionsScreenProps,
|
|
11
|
+
} from '../../screens/message_actions_screen'
|
|
12
|
+
import { MessageResource } from '../../types'
|
|
13
|
+
import {
|
|
14
|
+
AudioAdapter,
|
|
15
|
+
ChatAdapters,
|
|
16
|
+
ClipboardAdapter,
|
|
17
|
+
HapticAdapter,
|
|
18
|
+
ImagePickerAdapter,
|
|
19
|
+
VideoAdapter,
|
|
20
|
+
} from '../../utils/native_adapters'
|
|
21
|
+
|
|
22
|
+
const mockMessage: MessageResource = {
|
|
23
|
+
type: 'Message',
|
|
24
|
+
id: '1',
|
|
25
|
+
text: 'Copy me',
|
|
26
|
+
html: '<p>Copy me</p>',
|
|
27
|
+
createdAt: '2026-01-01T00:00:00Z',
|
|
28
|
+
deletedAt: null,
|
|
29
|
+
textEditedAt: null,
|
|
30
|
+
mine: false,
|
|
31
|
+
attachments: [],
|
|
32
|
+
author: { id: 1, type: 'Person', name: 'Author', avatar: '' },
|
|
33
|
+
reactionCounts: [],
|
|
34
|
+
replyCount: 0,
|
|
35
|
+
replyRootId: null,
|
|
36
|
+
messageType: 'user',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let mockCurrentMessage = mockMessage
|
|
40
|
+
|
|
41
|
+
const mockGoBack = jest.fn()
|
|
42
|
+
jest.mock('@react-navigation/native', () => ({
|
|
43
|
+
...jest.requireActual('@react-navigation/native'),
|
|
44
|
+
useNavigation: () => ({ navigate: jest.fn(), dispatch: jest.fn(), goBack: mockGoBack }),
|
|
45
|
+
}))
|
|
46
|
+
jest.mock('../../hooks/use_api_client', () => ({ useApiClient: () => ({}) }))
|
|
47
|
+
jest.mock('../../hooks/use_features', () => ({
|
|
48
|
+
...jest.requireActual('../../hooks/use_features'),
|
|
49
|
+
useFeatures: () => ({ features: [], featureEnabled: () => false }),
|
|
50
|
+
}))
|
|
51
|
+
jest.mock('../../hooks/use_message_reaction_toggle', () => ({
|
|
52
|
+
useMessageReactionToggle: () => ({ handleReactionToggle: jest.fn(), isPending: false }),
|
|
53
|
+
}))
|
|
54
|
+
jest.mock('../../hooks/use_conversation_messages', () => ({
|
|
55
|
+
useConversationMessages: () => ({ messages: [mockCurrentMessage], refetch: jest.fn() }),
|
|
56
|
+
}))
|
|
57
|
+
|
|
58
|
+
const setStringAsync = jest.fn(async (_: string) => {})
|
|
59
|
+
const notificationSuccess = jest.fn()
|
|
60
|
+
|
|
61
|
+
const safeAreaMetrics = {
|
|
62
|
+
frame: { x: 0, y: 0, width: 390, height: 844 },
|
|
63
|
+
insets: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const route = {
|
|
67
|
+
key: 'MessageActions',
|
|
68
|
+
name: 'MessageActions',
|
|
69
|
+
params: { conversation_id: 1, message_id: mockMessage.id },
|
|
70
|
+
} as MessageActionsScreenProps['route']
|
|
71
|
+
|
|
72
|
+
const renderScreen = () =>
|
|
73
|
+
render(
|
|
74
|
+
<QueryClientProvider client={buildTestQueryClient()}>
|
|
75
|
+
<SafeAreaProvider initialMetrics={safeAreaMetrics}>
|
|
76
|
+
<ThemeProvider value={DefaultTheme}>
|
|
77
|
+
<HeaderHeightContext.Provider value={0}>
|
|
78
|
+
<MessageActionsScreen route={route} />
|
|
79
|
+
</HeaderHeightContext.Provider>
|
|
80
|
+
</ThemeProvider>
|
|
81
|
+
</SafeAreaProvider>
|
|
82
|
+
</QueryClientProvider>
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
describe('MessageActionsScreen', () => {
|
|
86
|
+
beforeAll(() => {
|
|
87
|
+
ChatAdapters.configure({
|
|
88
|
+
clipboard: new ClipboardAdapter({ getStringAsync: async () => '', setStringAsync }),
|
|
89
|
+
audio: new AudioAdapter({ useAudio: () => ({}) as any }),
|
|
90
|
+
video: new VideoAdapter({ Player: React.forwardRef(() => null) }),
|
|
91
|
+
imagePicker: new ImagePickerAdapter({
|
|
92
|
+
openCameraAsync: async () => ({ canceled: true, assets: null }),
|
|
93
|
+
openImageLibraryAsync: async () => ({ canceled: true, assets: null }),
|
|
94
|
+
}),
|
|
95
|
+
haptic: new HapticAdapter({
|
|
96
|
+
impactLight: jest.fn(),
|
|
97
|
+
impactMedium: jest.fn(),
|
|
98
|
+
impactHeavy: jest.fn(),
|
|
99
|
+
rigid: jest.fn(),
|
|
100
|
+
soft: jest.fn(),
|
|
101
|
+
notificationSuccess,
|
|
102
|
+
notificationWarning: jest.fn(),
|
|
103
|
+
notificationError: jest.fn(),
|
|
104
|
+
}),
|
|
105
|
+
})
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
jest.clearAllMocks()
|
|
110
|
+
mockCurrentMessage = mockMessage
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('copies the message text and gives success haptic feedback before closing', async () => {
|
|
114
|
+
const { getByText } = renderScreen()
|
|
115
|
+
|
|
116
|
+
fireEvent.press(getByText('Copy text'))
|
|
117
|
+
|
|
118
|
+
expect(setStringAsync).toHaveBeenCalledWith('Copy me')
|
|
119
|
+
await waitFor(() => expect(mockGoBack).toHaveBeenCalledTimes(1))
|
|
120
|
+
expect(notificationSuccess).toHaveBeenCalledTimes(1)
|
|
121
|
+
expect(notificationSuccess.mock.invocationCallOrder[0]).toBeLessThan(
|
|
122
|
+
mockGoBack.mock.invocationCallOrder[0]
|
|
123
|
+
)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('leaves the clipboard alone and gives no haptic when there is nothing to copy', async () => {
|
|
127
|
+
mockCurrentMessage = { ...mockMessage, text: null }
|
|
128
|
+
const { getByText } = renderScreen()
|
|
129
|
+
|
|
130
|
+
fireEvent.press(getByText('Copy text'))
|
|
131
|
+
|
|
132
|
+
await waitFor(() => expect(mockGoBack).toHaveBeenCalledTimes(1))
|
|
133
|
+
expect(setStringAsync).not.toHaveBeenCalled()
|
|
134
|
+
expect(notificationSuccess).not.toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -28,6 +28,15 @@ import { getMessagesQueryKey } from '../../../utils/request/get_messages'
|
|
|
28
28
|
import { MessageList } from '../message_list'
|
|
29
29
|
|
|
30
30
|
jest.mock('../../../hooks/use_api_client')
|
|
31
|
+
const mockFlash = jest.fn()
|
|
32
|
+
jest.mock('../../../hooks/use_animated_message_background_color', () => ({
|
|
33
|
+
useAnimatedMessageBackgroundColor: () => ({
|
|
34
|
+
animatedBackgroundColor: {},
|
|
35
|
+
flash: mockFlash,
|
|
36
|
+
handleMessagePressIn: jest.fn(),
|
|
37
|
+
handleMessagePressOut: jest.fn(),
|
|
38
|
+
}),
|
|
39
|
+
}))
|
|
31
40
|
jest.mock('@react-navigation/native', () => ({
|
|
32
41
|
...jest.requireActual('@react-navigation/native'),
|
|
33
42
|
useNavigation: () => ({ navigate: jest.fn() }),
|
|
@@ -105,7 +114,7 @@ let queryClient: QueryClient
|
|
|
105
114
|
// A top-level rerender would rebuild the wrapper context and redraw every row.
|
|
106
115
|
let setHarnessConversation: (next: ConversationResource) => void = () => {}
|
|
107
116
|
|
|
108
|
-
function Harness() {
|
|
117
|
+
function Harness({ anchorMessageId }: { anchorMessageId?: string }) {
|
|
109
118
|
const [current, setCurrent] = React.useState(conversation)
|
|
110
119
|
React.useEffect(() => {
|
|
111
120
|
setHarnessConversation = setCurrent
|
|
@@ -117,6 +126,7 @@ function Harness() {
|
|
|
117
126
|
conversation={current}
|
|
118
127
|
conversationId={CONVERSATION_ID}
|
|
119
128
|
messages={loaded}
|
|
129
|
+
anchorMessageId={anchorMessageId}
|
|
120
130
|
hasNextPage
|
|
121
131
|
onEndReached={() => {}}
|
|
122
132
|
/>
|
|
@@ -151,16 +161,24 @@ const renderedRowIds = () => {
|
|
|
151
161
|
return Array.from(new Set(ids)).sort()
|
|
152
162
|
}
|
|
153
163
|
|
|
154
|
-
const renderList = (seed: MessageResource[] = messages) => {
|
|
164
|
+
const renderList = (seed: MessageResource[] = messages, anchorMessageId?: string) => {
|
|
155
165
|
queryClient.setQueryData<MessagesQueryData>(messagesQueryKey, {
|
|
156
166
|
pages: [{ data: seed, links: {}, meta: { count: seed.length, totalCount: seed.length } }],
|
|
157
167
|
pageParams: [{}],
|
|
158
168
|
})
|
|
159
|
-
render(<Harness />, { wrapper: Wrapper })
|
|
169
|
+
render(<Harness anchorMessageId={anchorMessageId} />, { wrapper: Wrapper })
|
|
160
170
|
seed.forEach(m => screen.getByTestId(`message-${m.id}`))
|
|
161
171
|
renderSpy.mockClear()
|
|
162
172
|
}
|
|
163
173
|
|
|
174
|
+
const reportViewable = (items: MessageResource[]) =>
|
|
175
|
+
act(() =>
|
|
176
|
+
screen.UNSAFE_getByType(FlatList).props.onViewableItemsChanged({
|
|
177
|
+
viewableItems: items.map(item => ({ item, key: item.id, index: null, isViewable: true })),
|
|
178
|
+
changed: [],
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
|
|
164
182
|
// TanStack notifies observers on a setTimeout; deliver inline so one write is one act().
|
|
165
183
|
beforeAll(() => notifyManager.setScheduler(callback => callback()))
|
|
166
184
|
afterAll(() => notifyManager.setScheduler(callback => setTimeout(callback, 0)))
|
|
@@ -274,4 +292,54 @@ describe('MessageList', () => {
|
|
|
274
292
|
expect(screen.queryByTestId(`message-${target.id}`)).toBeNull()
|
|
275
293
|
expect(renderedRowIds()).toEqual([])
|
|
276
294
|
})
|
|
295
|
+
|
|
296
|
+
describe('opened at an anchor message', () => {
|
|
297
|
+
const anchor = messages[4]
|
|
298
|
+
|
|
299
|
+
it('flashes the anchor row once it comes into view, redrawing only that row', () => {
|
|
300
|
+
renderList(messages, anchor.id)
|
|
301
|
+
|
|
302
|
+
reportViewable([messages[1], messages[2]])
|
|
303
|
+
expect(mockFlash).not.toHaveBeenCalled()
|
|
304
|
+
expect(renderedRowIds()).toEqual([])
|
|
305
|
+
|
|
306
|
+
reportViewable([messages[3], anchor])
|
|
307
|
+
expect(mockFlash).toHaveBeenCalledTimes(1)
|
|
308
|
+
expect(renderedRowIds()).toEqual([anchor.id])
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('flashes only once, through later viewability changes, redraws, and a remount', () => {
|
|
312
|
+
renderList(messages, anchor.id)
|
|
313
|
+
reportViewable([anchor])
|
|
314
|
+
expect(mockFlash).toHaveBeenCalledTimes(1)
|
|
315
|
+
|
|
316
|
+
reportViewable([anchor, messages[5]])
|
|
317
|
+
act(() => updateCacheWithReaction(queryClient, messagesQueryKey, reactionEvent(anchor), ME))
|
|
318
|
+
expect(screen.getByTestId(`reaction-${anchor.id}-heart`)).toBeTruthy()
|
|
319
|
+
|
|
320
|
+
act(() =>
|
|
321
|
+
queryClient.setQueryData<MessagesQueryData>(messagesQueryKey, prev =>
|
|
322
|
+
deleteRecordInPagesData({ data: prev, record: anchor })
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
expect(screen.queryByTestId(`message-${anchor.id}`)).toBeNull()
|
|
326
|
+
act(() =>
|
|
327
|
+
queryClient.setQueryData<MessagesQueryData>(messagesQueryKey, prev =>
|
|
328
|
+
addRecordInPagesData({ data: prev, record: anchor })
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
expect(screen.getByTestId(`message-${anchor.id}`)).toBeTruthy()
|
|
332
|
+
reportViewable([anchor])
|
|
333
|
+
|
|
334
|
+
expect(mockFlash).toHaveBeenCalledTimes(1)
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
it('never flashes when there is no anchor', () => {
|
|
338
|
+
renderList()
|
|
339
|
+
|
|
340
|
+
reportViewable([messages[0], messages[1]])
|
|
341
|
+
|
|
342
|
+
expect(mockFlash).not.toHaveBeenCalled()
|
|
343
|
+
})
|
|
344
|
+
})
|
|
277
345
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { PlatformPressable } from '@react-navigation/elements'
|
|
2
2
|
import React from 'react'
|
|
3
|
-
import { Text, StyleSheet
|
|
3
|
+
import { Text, StyleSheet } from 'react-native'
|
|
4
4
|
import { useTheme } from '../../../hooks'
|
|
5
5
|
import { DenormalizedGiphyAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
|
|
6
|
-
import { assertKeysAreNumbers } from '../../../utils'
|
|
6
|
+
import { assertKeysAreNumbers, Linking } from '../../../utils'
|
|
7
7
|
import { tokens } from '../../../vendor/tapestry/tokens'
|
|
8
8
|
import { Image } from '../../display'
|
|
9
9
|
|
|
@@ -22,7 +22,6 @@ export function GiphyAttachment({
|
|
|
22
22
|
|
|
23
23
|
function handlePress() {
|
|
24
24
|
if (titleLink) {
|
|
25
|
-
// Open Giphy link in the default web browser
|
|
26
25
|
Linking.openURL(titleLink)
|
|
27
26
|
}
|
|
28
27
|
}
|
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
StyleSheet,
|
|
7
7
|
Modal,
|
|
8
8
|
View,
|
|
9
|
-
Linking,
|
|
10
9
|
Dimensions,
|
|
11
10
|
NativeSyntheticEvent,
|
|
12
11
|
NativeScrollEvent,
|
|
@@ -30,7 +29,7 @@ import Animated, {
|
|
|
30
29
|
import { useSafeAreaInsets, SafeAreaProvider } from 'react-native-safe-area-context'
|
|
31
30
|
import { useTheme } from '../../../hooks'
|
|
32
31
|
import { DenormalizedMessageAttachmentResource } from '../../../types/resources/denormalized_attachment_resource'
|
|
33
|
-
import { Haptic, platformFontWeightMedium } from '../../../utils'
|
|
32
|
+
import { Haptic, Linking, platformFontWeightMedium } from '../../../utils'
|
|
34
33
|
import { formatDatePreview } from '../../../utils/date'
|
|
35
34
|
import { tokens } from '../../../vendor/tapestry/tokens'
|
|
36
35
|
import { IconButton, Image, Heading, Text } from '../../display'
|
|
@@ -212,7 +211,7 @@ const LightboxGallery = ({
|
|
|
212
211
|
HANDLERS
|
|
213
212
|
============================ */
|
|
214
213
|
const handleOpenInBrowser = useCallback(() => {
|
|
215
|
-
Linking.openURL(urlMedium || url)
|
|
214
|
+
Linking.openURL(urlMedium || url, { forceOpenInBrowser: true })
|
|
216
215
|
}, [urlMedium, url])
|
|
217
216
|
|
|
218
217
|
// Helper to enable or disable FlatList scroll state after gestures have ended
|
|
@@ -46,6 +46,8 @@ interface MessageProps extends MessageResource {
|
|
|
46
46
|
conversation_id: number
|
|
47
47
|
readByOthers?: boolean
|
|
48
48
|
inReplyScreen?: boolean
|
|
49
|
+
highlighted?: boolean
|
|
50
|
+
onHighlightShown?: () => void
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export const Message = memo(function MessageRow({
|
|
@@ -53,6 +55,8 @@ export const Message = memo(function MessageRow({
|
|
|
53
55
|
conversation_id,
|
|
54
56
|
readByOthers,
|
|
55
57
|
inReplyScreen,
|
|
58
|
+
highlighted = false,
|
|
59
|
+
onHighlightShown,
|
|
56
60
|
...message
|
|
57
61
|
}: MessageProps) {
|
|
58
62
|
const { reactionCounts, pending, error, attachments, author } = message
|
|
@@ -71,7 +75,7 @@ export const Message = memo(function MessageRow({
|
|
|
71
75
|
const isPersisted = !isNewMessage(message)
|
|
72
76
|
const [temporarilyHidePendingState, setTemporarilyHidePendingState] = React.useState(true)
|
|
73
77
|
const [messageBubbleHeight, setMessageBubbleHeight] = React.useState(0)
|
|
74
|
-
const { animatedBackgroundColor, handleMessagePressIn, handleMessagePressOut } =
|
|
78
|
+
const { animatedBackgroundColor, flash, handleMessagePressIn, handleMessagePressOut } =
|
|
75
79
|
useAnimatedMessageBackgroundColor()
|
|
76
80
|
const isReplyRootMessage = message.replyRootId === message.id
|
|
77
81
|
const isDeletedReplyRootMessage = isReplyRootMessage && !!message.deletedAt
|
|
@@ -116,6 +120,13 @@ export const Message = memo(function MessageRow({
|
|
|
116
120
|
messageText || ''
|
|
117
121
|
} ${saveStatusLabel} ${timestamp} ${replyCountLabel}`
|
|
118
122
|
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!highlighted) return
|
|
125
|
+
|
|
126
|
+
flash()
|
|
127
|
+
onHighlightShown?.()
|
|
128
|
+
}, [highlighted, flash, onHighlightShown])
|
|
129
|
+
|
|
119
130
|
useEffect(() => {
|
|
120
131
|
if (pending) {
|
|
121
132
|
const timer = setTimeout(() => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { useNavigation, useTheme as useNavigationTheme, useRoute } from '@react-navigation/native'
|
|
2
2
|
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'
|
|
3
3
|
import {
|
|
4
|
-
Linking,
|
|
5
4
|
Platform,
|
|
6
5
|
Pressable,
|
|
7
6
|
ScrollView,
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
FileAttachment,
|
|
35
34
|
} from '../../types/resources/denormalized_attachment_resource_for_create'
|
|
36
35
|
import {
|
|
36
|
+
Linking,
|
|
37
37
|
Log,
|
|
38
38
|
MAX_FONT_SIZE_MULTIPLIER_LANDMARK,
|
|
39
39
|
platformFontWeightMedium,
|