@jobber/components-native 0.40.1 → 0.42.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/dist/src/AutoLink/AutoLink.js +23 -0
- package/dist/src/AutoLink/clipboard.js +9 -0
- package/dist/src/AutoLink/components/ComposeTextWithLinks/ComposeTextWithLinks.js +19 -0
- package/dist/src/AutoLink/components/Link/Link.js +7 -0
- package/dist/src/AutoLink/components/index.js +2 -0
- package/dist/src/AutoLink/hooks/useCreateLinkedText.js +24 -0
- package/dist/src/AutoLink/hooks/useTokenGenerator.js +10 -0
- package/dist/src/AutoLink/index.js +1 -0
- package/dist/src/AutoLink/messages.js +18 -0
- package/dist/src/AutoLink/types.js +1 -0
- package/dist/src/AutoLink/utils.js +45 -0
- package/dist/src/FormatFile/FormatFile.js +114 -0
- package/dist/src/FormatFile/FormatFile.style.js +16 -0
- package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.js +8 -0
- package/dist/src/FormatFile/components/ErrorIcon/ErrorIcon.style.js +10 -0
- package/dist/src/FormatFile/components/ErrorIcon/index.js +1 -0
- package/dist/src/FormatFile/components/FileView/FileView.js +67 -0
- package/dist/src/FormatFile/components/FileView/FileView.style.js +64 -0
- package/dist/src/FormatFile/components/FileView/index.js +1 -0
- package/dist/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.js +22 -0
- package/dist/src/FormatFile/components/FormatFileBottomSheet/index.js +1 -0
- package/dist/src/FormatFile/components/FormatFileBottomSheet/messages.js +13 -0
- package/dist/src/FormatFile/components/MediaView/MediaView.js +56 -0
- package/dist/src/FormatFile/components/MediaView/MediaView.style.js +27 -0
- package/dist/src/FormatFile/components/MediaView/index.js +1 -0
- package/dist/src/FormatFile/components/ProgressBar/ProgressBar.js +29 -0
- package/dist/src/FormatFile/components/ProgressBar/ProgressBar.style.js +15 -0
- package/dist/src/FormatFile/components/ProgressBar/index.js +1 -0
- package/dist/src/FormatFile/components/_mocks/mockFiles.js +78 -0
- package/dist/src/FormatFile/constants.js +14 -0
- package/dist/src/FormatFile/context/FormatFileContext.js +8 -0
- package/dist/src/FormatFile/context/types.js +1 -0
- package/dist/src/FormatFile/index.js +1 -0
- package/dist/src/FormatFile/messages.js +23 -0
- package/dist/src/FormatFile/types.js +8 -0
- package/dist/src/FormatFile/utils/computeA11yLabel.js +12 -0
- package/dist/src/FormatFile/utils/createUseCreateThumbnail.js +22 -0
- package/dist/src/FormatFile/utils/index.js +1 -0
- package/dist/src/index.js +2 -0
- package/dist/src/utils/test/wait.js +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/src/AutoLink/AutoLink.d.ts +3 -0
- package/dist/types/src/AutoLink/clipboard.d.ts +2 -0
- package/dist/types/src/AutoLink/components/ComposeTextWithLinks/ComposeTextWithLinks.d.ts +3 -0
- package/dist/types/src/AutoLink/components/Link/Link.d.ts +8 -0
- package/dist/types/src/AutoLink/components/index.d.ts +2 -0
- package/dist/types/src/AutoLink/hooks/useCreateLinkedText.d.ts +12 -0
- package/dist/types/src/AutoLink/hooks/useTokenGenerator.d.ts +1 -0
- package/dist/types/src/AutoLink/index.d.ts +1 -0
- package/dist/types/src/AutoLink/messages.d.ts +17 -0
- package/dist/types/src/AutoLink/types.d.ts +32 -0
- package/dist/types/src/AutoLink/utils.d.ts +6 -0
- package/dist/types/src/Form/components/FormMessage/FormMessage.d.ts +1 -0
- package/dist/types/src/FormatFile/FormatFile.d.ts +47 -0
- package/dist/types/src/FormatFile/FormatFile.style.d.ts +14 -0
- package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.d.ts +2 -0
- package/dist/types/src/FormatFile/components/ErrorIcon/ErrorIcon.style.d.ts +8 -0
- package/dist/types/src/FormatFile/components/ErrorIcon/index.d.ts +1 -0
- package/dist/types/src/FormatFile/components/FileView/FileView.d.ts +12 -0
- package/dist/types/src/FormatFile/components/FileView/FileView.style.d.ts +62 -0
- package/dist/types/src/FormatFile/components/FileView/index.d.ts +1 -0
- package/dist/types/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.d.ts +11 -0
- package/dist/types/src/FormatFile/components/FormatFileBottomSheet/index.d.ts +2 -0
- package/dist/types/src/FormatFile/components/FormatFileBottomSheet/messages.d.ts +12 -0
- package/dist/types/src/FormatFile/components/MediaView/MediaView.d.ts +12 -0
- package/dist/types/src/FormatFile/components/MediaView/MediaView.style.d.ts +25 -0
- package/dist/types/src/FormatFile/components/MediaView/index.d.ts +1 -0
- package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.d.ts +19 -0
- package/dist/types/src/FormatFile/components/ProgressBar/ProgressBar.style.d.ts +13 -0
- package/dist/types/src/FormatFile/components/ProgressBar/index.d.ts +1 -0
- package/dist/types/src/FormatFile/components/_mocks/mockFiles.d.ts +18 -0
- package/dist/types/src/FormatFile/constants.d.ts +6 -0
- package/dist/types/src/FormatFile/context/FormatFileContext.d.ts +10 -0
- package/dist/types/src/FormatFile/context/types.d.ts +9 -0
- package/dist/types/src/FormatFile/index.d.ts +3 -0
- package/dist/types/src/FormatFile/messages.d.ts +22 -0
- package/dist/types/src/FormatFile/types.d.ts +105 -0
- package/dist/types/src/FormatFile/utils/computeA11yLabel.d.ts +9 -0
- package/dist/types/src/FormatFile/utils/createUseCreateThumbnail.d.ts +5 -0
- package/dist/types/src/FormatFile/utils/index.d.ts +1 -0
- package/dist/types/src/InputCurrency/InputCurrency.d.ts +1 -1
- package/dist/types/src/index.d.ts +2 -0
- package/package.json +5 -2
- package/src/AutoLink/AutoLink.test.tsx +203 -0
- package/src/AutoLink/AutoLink.tsx +36 -0
- package/src/AutoLink/clipboard.ts +14 -0
- package/src/AutoLink/components/ComposeTextWithLinks/ComposeTextWithLinks.tsx +38 -0
- package/src/AutoLink/components/Link/Link.test.tsx +30 -0
- package/src/AutoLink/components/Link/Link.tsx +21 -0
- package/src/AutoLink/components/index.ts +2 -0
- package/src/AutoLink/hooks/useCreateLinkedText.ts +35 -0
- package/src/AutoLink/hooks/useTokenGenerator.ts +11 -0
- package/src/AutoLink/index.ts +1 -0
- package/src/AutoLink/messages.ts +19 -0
- package/src/AutoLink/types.ts +39 -0
- package/src/AutoLink/utils.ts +63 -0
- package/src/FormatFile/FormatFile.style.ts +17 -0
- package/src/FormatFile/FormatFile.test.tsx +333 -0
- package/src/FormatFile/FormatFile.tsx +300 -0
- package/src/FormatFile/components/ErrorIcon/ErrorIcon.style.ts +11 -0
- package/src/FormatFile/components/ErrorIcon/ErrorIcon.tsx +12 -0
- package/src/FormatFile/components/ErrorIcon/index.ts +1 -0
- package/src/FormatFile/components/FileView/FileView.style.ts +65 -0
- package/src/FormatFile/components/FileView/FileView.tsx +134 -0
- package/src/FormatFile/components/FileView/index.ts +1 -0
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.test.tsx +108 -0
- package/src/FormatFile/components/FormatFileBottomSheet/FormatFileBottomSheet.tsx +56 -0
- package/src/FormatFile/components/FormatFileBottomSheet/index.ts +2 -0
- package/src/FormatFile/components/FormatFileBottomSheet/messages.ts +14 -0
- package/src/FormatFile/components/MediaView/MediaView.style.ts +28 -0
- package/src/FormatFile/components/MediaView/MediaView.tsx +145 -0
- package/src/FormatFile/components/MediaView/index.ts +1 -0
- package/src/FormatFile/components/ProgressBar/ProgressBar.style.tsx +16 -0
- package/src/FormatFile/components/ProgressBar/ProgressBar.tsx +57 -0
- package/src/FormatFile/components/ProgressBar/index.ts +1 -0
- package/src/FormatFile/components/_mocks/mockFiles.ts +105 -0
- package/src/FormatFile/constants.ts +15 -0
- package/src/FormatFile/context/FormatFileContext.ts +13 -0
- package/src/FormatFile/context/types.ts +12 -0
- package/src/FormatFile/index.ts +13 -0
- package/src/FormatFile/messages.ts +24 -0
- package/src/FormatFile/types.ts +126 -0
- package/src/FormatFile/utils/computeA11yLabel.ts +26 -0
- package/src/FormatFile/utils/createUseCreateThumbnail.ts +33 -0
- package/src/FormatFile/utils/index.ts +1 -0
- package/src/InputCurrency/InputCurrency.tsx +1 -1
- package/src/index.ts +2 -0
- package/src/utils/test/wait.ts +3 -1
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { copyTextToClipboard } from "./clipboard";
|
|
4
|
+
import { AutoLink } from "./AutoLink";
|
|
5
|
+
import { messages } from "./messages";
|
|
6
|
+
|
|
7
|
+
const mockOpenUrl = jest.fn();
|
|
8
|
+
jest.mock("react-native/Libraries/Linking/Linking", () => ({
|
|
9
|
+
openURL: mockOpenUrl,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock("./clipboard", () => {
|
|
13
|
+
return {
|
|
14
|
+
copyTextToClipboard: jest.fn(),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("AutoLink", () => {
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
mockOpenUrl.mockClear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("Urls", () => {
|
|
24
|
+
it("should find and link url amongst other text", () => {
|
|
25
|
+
const linkText = "getjobber.com";
|
|
26
|
+
const nonLinkText = "The best website is ";
|
|
27
|
+
const { getByText } = render(
|
|
28
|
+
<AutoLink>{`${nonLinkText}${linkText}`}</AutoLink>,
|
|
29
|
+
);
|
|
30
|
+
fireEvent.press(getByText(linkText));
|
|
31
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(`http://${linkText}`);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("should find and link a url without other text around it", () => {
|
|
35
|
+
const linkText = "getjobber.com";
|
|
36
|
+
const { getByText } = render(<AutoLink>{linkText}</AutoLink>);
|
|
37
|
+
fireEvent.press(getByText(linkText));
|
|
38
|
+
expect(mockOpenUrl).toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should copy link onLongPress", () => {
|
|
42
|
+
const linkText = "getjobber.com";
|
|
43
|
+
const nonLinkText = "The best website is ";
|
|
44
|
+
const { getByText } = render(
|
|
45
|
+
<AutoLink>{`${nonLinkText}${linkText}`}</AutoLink>,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
fireEvent(getByText(linkText), "onLongPress");
|
|
49
|
+
|
|
50
|
+
const expectedToastConfig = {
|
|
51
|
+
message: messages.urlCopied.defaultMessage,
|
|
52
|
+
bottomTabsVisible: true,
|
|
53
|
+
};
|
|
54
|
+
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
|
55
|
+
`http://${linkText}`,
|
|
56
|
+
expectedToastConfig,
|
|
57
|
+
);
|
|
58
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("should skip linking url if linkUrls is false", () => {
|
|
62
|
+
const linkText = "getjobber.com";
|
|
63
|
+
const nonLinkText = "The best website is ";
|
|
64
|
+
const { getByText } = render(
|
|
65
|
+
<AutoLink urls={false}>{`${nonLinkText}${linkText}`}</AutoLink>,
|
|
66
|
+
);
|
|
67
|
+
fireEvent.press(getByText(RegExp(linkText, "i")));
|
|
68
|
+
|
|
69
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe("Emails", () => {
|
|
74
|
+
it("should find and link emails in text", () => {
|
|
75
|
+
const nonEmailText = "The best email is ";
|
|
76
|
+
const emailText = "test@example.com";
|
|
77
|
+
const { getByText } = render(
|
|
78
|
+
<AutoLink>{`${nonEmailText}${emailText}`}</AutoLink>,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
fireEvent.press(getByText(emailText));
|
|
82
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(
|
|
83
|
+
`mailto:${encodeURIComponent(emailText)}`,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("should find and link an email without other text around it", () => {
|
|
88
|
+
const emailText = "test@example.com";
|
|
89
|
+
const { getByText } = render(<AutoLink>{emailText}</AutoLink>);
|
|
90
|
+
|
|
91
|
+
fireEvent.press(getByText(emailText));
|
|
92
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(
|
|
93
|
+
`mailto:${encodeURIComponent(emailText)}`,
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should copy email onLongPress", () => {
|
|
98
|
+
const nonEmailText = "The best email is ";
|
|
99
|
+
const emailText = "test@example.com";
|
|
100
|
+
const { getByText } = render(
|
|
101
|
+
<AutoLink>{`${nonEmailText}${emailText}`}</AutoLink>,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
fireEvent(getByText(emailText), "onLongPress");
|
|
105
|
+
|
|
106
|
+
const expectedToastConfig = {
|
|
107
|
+
message: messages.emailCopied.defaultMessage,
|
|
108
|
+
bottomTabsVisible: true,
|
|
109
|
+
};
|
|
110
|
+
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
|
111
|
+
emailText,
|
|
112
|
+
expectedToastConfig,
|
|
113
|
+
);
|
|
114
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should skip linking email if linkEmails is false", () => {
|
|
118
|
+
const nonEmailText = "The best email is ";
|
|
119
|
+
const emailText = "test@example.com";
|
|
120
|
+
const { getByText } = render(
|
|
121
|
+
<AutoLink email={false}>{`${nonEmailText}${emailText}`}</AutoLink>,
|
|
122
|
+
);
|
|
123
|
+
fireEvent.press(getByText(RegExp(emailText, "i")));
|
|
124
|
+
|
|
125
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Phone Numbers", () => {
|
|
130
|
+
it("should find and link phone numbers in text", () => {
|
|
131
|
+
const nonPhoneText = "The best phone number is ";
|
|
132
|
+
const phoneText = "902-555-5555";
|
|
133
|
+
const { getByText } = render(
|
|
134
|
+
<AutoLink>{`${nonPhoneText}${phoneText}`}</AutoLink>,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
const expectedPhone = phoneText.replace(/-/g, "");
|
|
138
|
+
fireEvent.press(getByText(phoneText));
|
|
139
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(`tel:${expectedPhone}`);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should find and link a phone number without other text around it", () => {
|
|
143
|
+
const phoneText = "902-555-5555";
|
|
144
|
+
const { getByText } = render(<AutoLink>{phoneText}</AutoLink>);
|
|
145
|
+
|
|
146
|
+
const expectedPhone = phoneText.replace(/-/g, "");
|
|
147
|
+
fireEvent.press(getByText(phoneText));
|
|
148
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(`tel:${expectedPhone}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("should copy phone number onLongPress", () => {
|
|
152
|
+
const nonPhoneText = "The best phone number is ";
|
|
153
|
+
const phoneText = "902-555-5555";
|
|
154
|
+
const { getByText } = render(
|
|
155
|
+
<AutoLink>{`${nonPhoneText}${phoneText}`}</AutoLink>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
fireEvent(getByText(phoneText), "onLongPress");
|
|
159
|
+
|
|
160
|
+
const expectedToastConfig = {
|
|
161
|
+
message: messages.phoneCopied.defaultMessage,
|
|
162
|
+
bottomTabsVisible: true,
|
|
163
|
+
};
|
|
164
|
+
expect(copyTextToClipboard).toHaveBeenCalledWith(
|
|
165
|
+
phoneText.replace(/-/g, ""),
|
|
166
|
+
expectedToastConfig,
|
|
167
|
+
);
|
|
168
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should skip linking phone number if linkPhones is false", () => {
|
|
172
|
+
const nonPhoneText = "The best phone number is ";
|
|
173
|
+
const phoneText = "902-555-5555";
|
|
174
|
+
const { getByText } = render(
|
|
175
|
+
<AutoLink phone={false}>{`${nonPhoneText}${phoneText}`}</AutoLink>,
|
|
176
|
+
);
|
|
177
|
+
fireEvent.press(getByText(RegExp(phoneText, "i")));
|
|
178
|
+
|
|
179
|
+
expect(mockOpenUrl).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("should properly link a combination of urls, emails and phone numbers", () => {
|
|
184
|
+
const phoneText = "902-555-5555";
|
|
185
|
+
const emailText = "test@example.com";
|
|
186
|
+
const urlText = "getjobber.com";
|
|
187
|
+
const { getByText } = render(
|
|
188
|
+
<AutoLink>{`${phoneText} ${emailText} ${urlText}`}</AutoLink>,
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const expectedPhone = phoneText.replace(/-/g, "");
|
|
192
|
+
fireEvent.press(getByText(phoneText));
|
|
193
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(`tel:${expectedPhone}`);
|
|
194
|
+
|
|
195
|
+
fireEvent.press(getByText(emailText));
|
|
196
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(
|
|
197
|
+
`mailto:${encodeURIComponent(emailText)}`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
fireEvent.press(getByText(urlText));
|
|
201
|
+
expect(mockOpenUrl).toHaveBeenCalledWith(`http://${urlText}`);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text as RNText } from "react-native";
|
|
3
|
+
import { AutoLinkProps } from "./types";
|
|
4
|
+
import { ComposeTextWithLinks } from "./components";
|
|
5
|
+
import { useCreateLinkedText } from "./hooks/useCreateLinkedText";
|
|
6
|
+
import { TypographyGestureDetector } from "../Typography";
|
|
7
|
+
import { tokens } from "../utils/design";
|
|
8
|
+
|
|
9
|
+
export function AutoLink({
|
|
10
|
+
children: text = "",
|
|
11
|
+
bottomTabsVisible = true,
|
|
12
|
+
selectable = true,
|
|
13
|
+
...rest
|
|
14
|
+
}: AutoLinkProps): JSX.Element {
|
|
15
|
+
const { splitText, matches } = useCreateLinkedText({ text, ...rest });
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<TypographyGestureDetector>
|
|
19
|
+
<RNText
|
|
20
|
+
selectable={selectable}
|
|
21
|
+
selectionColor={tokens["color-brand--highlight"]}
|
|
22
|
+
>
|
|
23
|
+
{splitText.map((part, index) => (
|
|
24
|
+
<ComposeTextWithLinks
|
|
25
|
+
key={index}
|
|
26
|
+
part={part}
|
|
27
|
+
index={index}
|
|
28
|
+
match={matches[part]}
|
|
29
|
+
bottomTabsVisible={bottomTabsVisible}
|
|
30
|
+
selectable={selectable}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</RNText>
|
|
34
|
+
</TypographyGestureDetector>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import Clipboard from "@react-native-clipboard/clipboard";
|
|
2
|
+
import { ShowToastParams, showToast } from "../Toast";
|
|
3
|
+
|
|
4
|
+
export function copyTextToClipboard(
|
|
5
|
+
text: string,
|
|
6
|
+
toastConfig?: ShowToastParams,
|
|
7
|
+
): void {
|
|
8
|
+
Clipboard.setString(text);
|
|
9
|
+
|
|
10
|
+
if (toastConfig) {
|
|
11
|
+
const { message, bottomTabsVisible } = toastConfig;
|
|
12
|
+
showToast({ message, bottomTabsVisible });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useIntl } from "react-intl";
|
|
3
|
+
import { Platform } from "react-native";
|
|
4
|
+
import { ComposeTextWithLinksProps } from "../../types";
|
|
5
|
+
import { onLongPressLink, onPressLink } from "../../utils";
|
|
6
|
+
import { Link } from "../Link/Link";
|
|
7
|
+
import { Text } from "../../../Text";
|
|
8
|
+
|
|
9
|
+
export function ComposeTextWithLinks({
|
|
10
|
+
part,
|
|
11
|
+
index,
|
|
12
|
+
match,
|
|
13
|
+
bottomTabsVisible,
|
|
14
|
+
selectable = true,
|
|
15
|
+
}: ComposeTextWithLinksProps): JSX.Element {
|
|
16
|
+
const { formatMessage } = useIntl();
|
|
17
|
+
|
|
18
|
+
const isLink = match?.getType();
|
|
19
|
+
|
|
20
|
+
if (isLink) {
|
|
21
|
+
return (
|
|
22
|
+
<Link
|
|
23
|
+
key={index}
|
|
24
|
+
onPress={() => onPressLink(match)}
|
|
25
|
+
onLongPress={() => {
|
|
26
|
+
if (selectable && Platform.OS === "android") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
onLongPressLink(match, bottomTabsVisible, formatMessage);
|
|
30
|
+
}}
|
|
31
|
+
>
|
|
32
|
+
{match.getAnchorText()}
|
|
33
|
+
</Link>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return <Text key={index}>{part}</Text>;
|
|
38
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { fireEvent, render } from "@testing-library/react-native";
|
|
3
|
+
import { Link } from "./Link";
|
|
4
|
+
|
|
5
|
+
describe("Link", () => {
|
|
6
|
+
const textLink = "getjobber.com";
|
|
7
|
+
const mockPress = jest.fn();
|
|
8
|
+
|
|
9
|
+
it("should call the onPress callback", () => {
|
|
10
|
+
const { getAllByText } = render(
|
|
11
|
+
<Link onPress={mockPress}>{textLink}</Link>,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
fireEvent.press(getAllByText("getjobber.com")[0]);
|
|
15
|
+
expect(mockPress).toHaveBeenCalled();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("should call the longPress callback", () => {
|
|
19
|
+
const mockLongPress = jest.fn();
|
|
20
|
+
const { getAllByText } = render(
|
|
21
|
+
<Link onPress={mockPress} onLongPress={mockLongPress}>
|
|
22
|
+
{textLink}
|
|
23
|
+
</Link>,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
fireEvent(getAllByText(textLink)[0], "onLongPress");
|
|
27
|
+
|
|
28
|
+
expect(mockLongPress).toHaveBeenCalled();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Text as RNText } from "react-native";
|
|
3
|
+
import { Text } from "../../../Text";
|
|
4
|
+
|
|
5
|
+
interface LinkProps {
|
|
6
|
+
readonly children: string;
|
|
7
|
+
readonly onPress: () => void;
|
|
8
|
+
readonly onLongPress?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Link({
|
|
12
|
+
children,
|
|
13
|
+
onPress,
|
|
14
|
+
onLongPress,
|
|
15
|
+
}: LinkProps): JSX.Element {
|
|
16
|
+
return (
|
|
17
|
+
<RNText onPress={onPress} onLongPress={onLongPress}>
|
|
18
|
+
<Text variation="interactive">{children}</Text>
|
|
19
|
+
</RNText>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Autolinker, { Match } from "autolinker";
|
|
2
|
+
import { useTokenGenerator } from "./useTokenGenerator";
|
|
3
|
+
import { shouldIgnoreURL } from "../utils";
|
|
4
|
+
|
|
5
|
+
export function useCreateLinkedText({
|
|
6
|
+
text = "",
|
|
7
|
+
email = true,
|
|
8
|
+
phone = true,
|
|
9
|
+
urls = true,
|
|
10
|
+
}) {
|
|
11
|
+
const [generateToken, tokenRegexp] = useTokenGenerator();
|
|
12
|
+
|
|
13
|
+
const matches: { [token: string]: Match } = {};
|
|
14
|
+
|
|
15
|
+
const linkedText = Autolinker.link(text, {
|
|
16
|
+
email,
|
|
17
|
+
phone,
|
|
18
|
+
urls,
|
|
19
|
+
replaceFn: match => {
|
|
20
|
+
if (shouldIgnoreURL(text, match)) return false;
|
|
21
|
+
|
|
22
|
+
const token = generateToken();
|
|
23
|
+
matches[token] = match;
|
|
24
|
+
return token;
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const splitText = splitLinkedText(linkedText, tokenRegexp);
|
|
29
|
+
|
|
30
|
+
return { splitText, matches };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function splitLinkedText(linkedText: string, tokenRegexp: RegExp): string[] {
|
|
34
|
+
return linkedText.split(tokenRegexp).filter(part => Boolean(part));
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { v1 } from "react-native-uuid";
|
|
3
|
+
|
|
4
|
+
export function useTokenGenerator(): [() => string, RegExp] {
|
|
5
|
+
let counter = 0;
|
|
6
|
+
const [identifier] = useState(v1());
|
|
7
|
+
return [
|
|
8
|
+
() => `@__ELEMENT-${identifier}-${counter++}__@`,
|
|
9
|
+
new RegExp(`(@__ELEMENT-${identifier}-\\d+__@)`, "g"),
|
|
10
|
+
];
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./AutoLink";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineMessages } from "react-intl";
|
|
2
|
+
|
|
3
|
+
export const messages = defineMessages({
|
|
4
|
+
phoneCopied: {
|
|
5
|
+
id: "phoneCopied",
|
|
6
|
+
defaultMessage: "Phone number copied",
|
|
7
|
+
description: "Message shown after copying a phone number",
|
|
8
|
+
},
|
|
9
|
+
emailCopied: {
|
|
10
|
+
id: "emailCopied",
|
|
11
|
+
defaultMessage: "Email copied",
|
|
12
|
+
description: "Message shown after copying an email",
|
|
13
|
+
},
|
|
14
|
+
urlCopied: {
|
|
15
|
+
id: "urlCopied",
|
|
16
|
+
defaultMessage: "URL copied",
|
|
17
|
+
description: "Message shown after copying a URL",
|
|
18
|
+
},
|
|
19
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Match } from "autolinker";
|
|
2
|
+
import { TextProps } from "react-native";
|
|
3
|
+
|
|
4
|
+
export interface AutoLinkProps extends Pick<TextProps, "selectable"> {
|
|
5
|
+
/**
|
|
6
|
+
* Text to display.
|
|
7
|
+
*/
|
|
8
|
+
readonly children: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Flag for linking phone numbers in text
|
|
12
|
+
*/
|
|
13
|
+
readonly phone?: boolean;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Flag for linking emails in text
|
|
17
|
+
*/
|
|
18
|
+
readonly email?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Flag for linking urls in text
|
|
22
|
+
*/
|
|
23
|
+
readonly urls?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Determines the placement of the toast that gets shown onLongPress
|
|
27
|
+
*/
|
|
28
|
+
readonly bottomTabsVisible?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type LinkType = "email" | "phone" | "url";
|
|
32
|
+
|
|
33
|
+
export interface ComposeTextWithLinksProps {
|
|
34
|
+
part: string;
|
|
35
|
+
index: number;
|
|
36
|
+
match: Match;
|
|
37
|
+
bottomTabsVisible: boolean;
|
|
38
|
+
selectable?: boolean;
|
|
39
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { EmailMatch, Match, PhoneMatch } from "autolinker";
|
|
2
|
+
import { MessageDescriptor } from "react-intl";
|
|
3
|
+
import { Linking } from "react-native";
|
|
4
|
+
import { messages } from "./messages";
|
|
5
|
+
import { LinkType } from "./types";
|
|
6
|
+
import { copyTextToClipboard } from "./clipboard";
|
|
7
|
+
|
|
8
|
+
function hasPrefix(text: string, prefixes: string[]): boolean {
|
|
9
|
+
return prefixes.some(prefix => text.includes(prefix));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function shouldIgnoreURL(text: string, match: Match): boolean {
|
|
13
|
+
const matchedText = match.getMatchedText().toLocaleLowerCase();
|
|
14
|
+
const urlPrefixes = ["http://", "https://", "www."];
|
|
15
|
+
const ignorePrefixes = ["file://", "ftp://"];
|
|
16
|
+
const previousChar = text.charAt(match.getOffset() - 1);
|
|
17
|
+
|
|
18
|
+
if (match.getType() === "url") {
|
|
19
|
+
const hasUrlPrefix = hasPrefix(matchedText, urlPrefixes);
|
|
20
|
+
const hasIgnorePrefix =
|
|
21
|
+
hasPrefix(matchedText, ignorePrefixes) || previousChar === "/";
|
|
22
|
+
|
|
23
|
+
return hasIgnorePrefix && !hasUrlPrefix;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getUrl(match: Match, immediateOpen = true): string {
|
|
30
|
+
const matchType = match.getType();
|
|
31
|
+
|
|
32
|
+
switch (matchType) {
|
|
33
|
+
case "email":
|
|
34
|
+
return immediateOpen
|
|
35
|
+
? `mailto:${encodeURIComponent((match as EmailMatch).getEmail())}`
|
|
36
|
+
: (match as EmailMatch).getEmail();
|
|
37
|
+
case "phone":
|
|
38
|
+
return immediateOpen
|
|
39
|
+
? `tel:${(match as PhoneMatch).getNumber()}`
|
|
40
|
+
: (match as PhoneMatch).getNumber();
|
|
41
|
+
default:
|
|
42
|
+
return match.getAnchorHref();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function onLongPressLink(
|
|
47
|
+
match: Match,
|
|
48
|
+
bottomTabsVisible: boolean,
|
|
49
|
+
formatMessage: (message: MessageDescriptor) => string,
|
|
50
|
+
): void {
|
|
51
|
+
const linkUrl = getUrl(match, false);
|
|
52
|
+
|
|
53
|
+
const toastConfig = {
|
|
54
|
+
message: formatMessage(messages[`${match.getType() as LinkType}Copied`]),
|
|
55
|
+
bottomTabsVisible,
|
|
56
|
+
};
|
|
57
|
+
copyTextToClipboard(linkUrl, toastConfig);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function onPressLink(match: Match): void {
|
|
61
|
+
const linkUrl = getUrl(match);
|
|
62
|
+
Linking.openURL(linkUrl);
|
|
63
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StyleSheet } from "react-native";
|
|
2
|
+
import { tokens } from "../utils/design";
|
|
3
|
+
|
|
4
|
+
export const styles = StyleSheet.create({
|
|
5
|
+
thumbnailContainer: {
|
|
6
|
+
backgroundColor: tokens["color-surface--background"],
|
|
7
|
+
borderWidth: tokens["border-base"],
|
|
8
|
+
borderColor: tokens["color-border"],
|
|
9
|
+
borderRadius: tokens["radius-base"],
|
|
10
|
+
marginBottom: tokens["space-small"],
|
|
11
|
+
},
|
|
12
|
+
thumbnailContainerGrid: {
|
|
13
|
+
width: tokens["space-extravagant"],
|
|
14
|
+
height: tokens["space-extravagant"],
|
|
15
|
+
marginRight: tokens["space-small"],
|
|
16
|
+
},
|
|
17
|
+
});
|