Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bold-quotes-post.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Add a `content` prop to `TumblrShareButton` and `buildTumblrShareUrl`, and list the other Tumblr post types for `posttype`. For post types other than `link`, `content` carries the actual post data, such as the quote text. Based on #564 by @me4502.
5 changes: 5 additions & 0 deletions .changeset/kind-mails-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Add a `to` prop to `EmailShareButton` and `buildEmailShareUrl` for prefilling the recipient. It accepts one address or an array of addresses. Requested in #418.
5 changes: 5 additions & 0 deletions .changeset/open-new-tabs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Add an `openInNewTab` prop to popup-based share buttons, and a matching `newTab` option for `openShareWindow`, to open the share page in a regular browser tab instead of a popup window. Requested in #449 and proposed in #342.
5 changes: 5 additions & 0 deletions .changeset/purple-casts-share.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Add `FarcasterShareButton`, `FarcasterIcon`, `FarcasterLegacyIcon`, and `buildFarcasterShareUrl` for sharing to Farcaster (formerly Warpcast) through its compose intent. The shared URL is attached as an embed, and `text`, `channelKey`, and `parentCastHash` are supported. Requested in #558.
5 changes: 5 additions & 0 deletions .changeset/quiet-posts-text.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Make `url` optional on `XShareButton` and `buildXShareUrl` so text-only posts can be shared by passing only `text`. At least one of `url` or `text` is still required. Requested in #570.
5 changes: 5 additions & 0 deletions .changeset/warm-phones-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-share': minor
---

Add a `phone` prop to `WhatsappShareButton` and `buildWhatsappShareUrl` for sharing to a specific number. It takes the number in international format and removes everything except digits. It applies to both the web and the app link. Requested in #359.
152 changes: 110 additions & 42 deletions README.md

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions demo/Demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
FacebookMessengerIcon,
FacebookMessengerShareButton,
FacebookShareButton,
FarcasterIcon,
FarcasterShareButton,
GabIcon,
GabShareButton,
HatenaIcon,
Expand Down Expand Up @@ -194,6 +196,19 @@ export function Demo() {
</BlueskyShareButton>
</div>

<div className="Demo__some-network">
<FarcasterShareButton
aria-label="Share on Farcaster"
url={shareUrl}
text={title}
windowWidth={660}
windowHeight={460}
className="Demo__some-network__share-button"
>
<FarcasterIcon size={32} />
</FarcasterShareButton>
</div>

<div className="Demo__some-network">
<ViberShareButton
aria-label="Share on Viber"
Expand Down
25 changes: 13 additions & 12 deletions example.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 21 additions & 4 deletions src/EmailShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,35 @@ type Options = {
body?: string;
separator?: string;
subject?: string;
/** Recipient address, or several addresses. */
to?: string | string[];
};

type EmailShareButtonProps = Omit<ShareButtonProps<Options>, 'onClick' | 'openShareDialogOnClick'> &
type EmailShareButtonProps = Omit<
ShareButtonProps<Options>,
'onClick' | 'openInNewTab' | 'openShareDialogOnClick'
> &
Options;

function emailLink(url: string, { subject, body, separator = '\n' }: Options) {
return 'mailto:' + objectToGetParams({ subject, body: body ? body + separator + url : url });
function emailRecipients(to: Options['to']) {
return (Array.isArray(to) ? to : [to])
.filter(Boolean)
.map(address => encodeURIComponent(address as string).replace(/%40/g, '@'))
.join(',');
}

function emailLink(url: string, { subject, body, separator = '\n', to }: Options) {
return (
'mailto:' +
emailRecipients(to) +
objectToGetParams({ subject, body: body ? body + separator + url : url })
);
}

export const buildEmailShareUrl = emailLink;

export const EmailShareButton = forwardRef<HTMLButtonElement, EmailShareButtonProps>(
({ body, separator, subject, ...props }, ref) => (
({ body, separator, subject, to, ...props }, ref) => (
<ShareButton
{...props}
forwardedRef={ref}
Expand All @@ -32,6 +48,7 @@ export const EmailShareButton = forwardRef<HTMLButtonElement, EmailShareButtonPr
subject,
body,
separator: separator || '\n',
to,
}}
/>
),
Expand Down
14 changes: 14 additions & 0 deletions src/FarcasterIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RawIconBase, type RawIconProps } from './icons/RawIconBase';

export function FarcasterIcon(props: RawIconProps) {
return (
<RawIconBase
backdrop={<rect x="4" y="4" width="16" height="16" />}
color="#855DCD"
viewBox="0 0 24 24"
{...props}
>
<path d="M18.24.24H5.76C2.5789.24 0 2.8188 0 6v12c0 3.1811 2.5789 5.76 5.76 5.76h12.48c3.1812 0 5.76-2.5789 5.76-5.76V6C24 2.8188 21.4212.24 18.24.24m.8155 17.1662v.504c.2868-.0256.5458.1905.5439.479v.5688h-5.1437v-.5688c-.0019-.2885.2576-.5047.5443-.479v-.504c0-.22.1525-.402.358-.458l-.0095-4.3645c-.1589-1.7366-1.6402-3.0979-3.4435-3.0979-1.8038 0-3.2846 1.3613-3.4435 3.0979l-.0096 4.3578c.2276.0424.5318.2083.5395.4648v.504c.2863-.0256.5457.1905.5438.479v.5688H4.3915v-.5688c-.0019-.2885.2575-.5047.5438-.479v-.504c0-.2529.2011-.4548.4536-.4724v-7.895h-.4905L4.2898 7.008l2.6405-.0005V5.0419h9.9495v1.9656h2.8219l-.6091 2.0314h-.4901v7.8949c.2519.0177.453.2195.453.4724" />
</RawIconBase>
);
}
11 changes: 11 additions & 0 deletions src/FarcasterLegacyIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { LegacyIconBase, type LegacyIconProps } from './icons/LegacyIconBase';

export function FarcasterLegacyIcon(props: LegacyIconProps) {
return (
<LegacyIconBase color="#855DCD" {...props}>
<g transform="translate(32 32) scale(2.25) translate(-12 -12.1)">
<path d="M19.0555 17.4062v.504c.2868-.0256.5458.1905.5439.479v.5688h-5.1437v-.5688c-.0019-.2885.2576-.5047.5443-.479v-.504c0-.22.1525-.402.358-.458l-.0095-4.3645c-.1589-1.7366-1.6402-3.0979-3.4435-3.0979-1.8038 0-3.2846 1.3613-3.4435 3.0979l-.0096 4.3578c.2276.0424.5318.2083.5395.4648v.504c.2863-.0256.5457.1905.5438.479v.5688H4.3915v-.5688c-.0019-.2885.2575-.5047.5438-.479v-.504c0-.2529.2011-.4548.4536-.4724v-7.895h-.4905L4.2898 7.008l2.6405-.0005V5.0419h9.9495v1.9656h2.8219l-.6091 2.0314h-.4901v7.8949c.2519.0177.453.2195.453.4724" />
</g>
</LegacyIconBase>
);
}
50 changes: 50 additions & 0 deletions src/FarcasterShareButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { forwardRef } from 'react';
import { assert } from './utils/assert';
import { objectToGetParams } from './utils/objectToGetParams';
import { ShareButton, type ShareButtonProps } from './ShareButton';

type FarcasterOptions = {
channelKey?: string;
parentCastHash?: string;
text?: string;
};

type FarcasterShareButtonProps = Omit<ShareButtonProps<FarcasterOptions>, 'title'> &
FarcasterOptions;

function farcasterLink(url: string, { channelKey, parentCastHash, text }: FarcasterOptions) {
assert(url, 'farcaster.url');

return (
'https://farcaster.xyz/~/compose' +
objectToGetParams({
text,
'embeds[]': url,
channelKey,
parentCastHash,
}).replace('embeds%5B%5D=', 'embeds[]=')
);
}

export const buildFarcasterShareUrl = farcasterLink;

export const FarcasterShareButton = forwardRef<HTMLButtonElement, FarcasterShareButtonProps>(
({ channelKey, parentCastHash, text, ...props }, ref) => (
<ShareButton
{...props}
forwardedRef={ref}
networkName="farcaster"
networkLink={buildFarcasterShareUrl}
opts={{
channelKey,
parentCastHash,
text,
}}
windowHeight={460}
windowPosition="windowCenter"
windowWidth={660}
/>
),
);

FarcasterShareButton.displayName = 'FarcasterShareButton';
9 changes: 9 additions & 0 deletions src/ShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const DEFAULT_ARIA_LABELS = {
email: 'Share by email',
facebook: 'Share on Facebook',
facebookmessenger: 'Share in Messenger',
farcaster: 'Share on Farcaster',
gab: 'Share on Gab',
hatena: 'Share on Hatena',
instapaper: 'Save to Instapaper',
Expand Down Expand Up @@ -71,6 +72,12 @@ export interface Props<LinkOptions> extends NativeButtonProps {
*/
onShareWindowOpen?: (shareWindow: Window | null) => void;
openShareDialogOnClick?: boolean;
/**
* Opens the share page in a regular browser tab instead of a popup window.
* `windowWidth`, `windowHeight`, and `windowPosition` are ignored in that case.
* @default false
*/
openInNewTab?: boolean;
opts: LinkOptions;
resetButtonStyle?: boolean;
/**
Expand Down Expand Up @@ -104,6 +111,7 @@ export function ShareButton<LinkOptions extends Record<string, unknown>>({
onClick,
onShareWindowClose,
onShareWindowOpen,
openInNewTab = false,
openShareDialogOnClick = true,
opts,
resetButtonStyle = true,
Expand Down Expand Up @@ -136,6 +144,7 @@ export function ShareButton<LinkOptions extends Record<string, unknown>>({
const opensPopup = openShareDialogOnClick && linkBehavior === 'popup';
const windowOptions = {
height: windowHeight,
newTab: openInNewTab,
onClose: onShareWindowClose,
position: windowPosition,
width: windowWidth,
Expand Down
19 changes: 16 additions & 3 deletions src/TumblrShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,23 @@ import { assert } from './utils/assert';
import { objectToGetParams } from './utils/objectToGetParams';
import { ShareButton, type ShareButtonProps } from './ShareButton';

type TumblrPostType = 'link' | 'text' | 'quote' | 'photo' | 'chat' | 'video' | (string & {});

function tumblrLink(
url: string,
{
postTitle,
caption,
tags,
posttype,
}: { postTitle?: string; caption?: string; tags?: string; posttype?: 'link' | string },
content,
}: {
postTitle?: string;
caption?: string;
tags?: string;
posttype?: TumblrPostType;
content?: string;
},
) {
assert(url, 'tumblr.url');

Expand All @@ -22,6 +31,7 @@ function tumblrLink(
caption,
tags,
posttype,
content,
})
);
}
Expand All @@ -31,14 +41,16 @@ export const buildTumblrShareUrl = tumblrLink;
type Options = {
postTitle?: string;
caption?: string;
posttype?: 'link' | string;
/** The post content for post types other than `link`, such as the quote text or photo URL. */
content?: string;
posttype?: TumblrPostType;
};

type TumblrShareButtonProps = Omit<ShareButtonProps<Options & { tags: string }>, 'title'> &
Options & { tags?: string[] };

export const TumblrShareButton = forwardRef<HTMLButtonElement, TumblrShareButtonProps>(
({ caption, postTitle, posttype, tags, ...props }, ref) => (
({ caption, content, postTitle, posttype, tags, ...props }, ref) => (
<ShareButton
{...props}
forwardedRef={ref}
Expand All @@ -49,6 +61,7 @@ export const TumblrShareButton = forwardRef<HTMLButtonElement, TumblrShareButton
tags: (tags || []).join(','),
caption,
posttype: posttype || 'link',
content,
}}
windowHeight={460}
windowWidth={660}
Expand Down
28 changes: 15 additions & 13 deletions src/WhatsappShareButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,46 @@ import { assert } from './utils/assert';
import { objectToGetParams } from './utils/objectToGetParams';
import { ShareButton, type ShareButtonProps } from './ShareButton';

type WhatsappShareButtonProps = Omit<
ShareButtonProps<{ message?: string; separator?: string }>,
'title'
> & {
type WhatsappOptions = { message?: string; phone?: string; separator?: string };

type WhatsappShareButtonProps = Omit<ShareButtonProps<WhatsappOptions>, 'title'> & {
message?: string;
mobileBehavior?: MobileBehavior;
/** Recipient phone number in international format, for example `+358401234567`. */
phone?: string;
separator?: string;
};

function whatsappLink(
url: string,
{ message, separator = ' ' }: { message?: string; separator?: string },
) {
function whatsappPhone(phone: string | undefined) {
return phone?.replace(/\D/g, '') || undefined;
}

function whatsappLink(url: string, { message, phone, separator = ' ' }: WhatsappOptions) {
assert(url, 'whatsapp.url');
return (
'https://api.whatsapp.com/send' +
objectToGetParams({
phone: whatsappPhone(phone),
text: message ? message + separator + url : url,
})
);
}

export const buildWhatsappShareUrl = whatsappLink;

function whatsappAppLink(
url: string,
{ message, separator = ' ' }: { message?: string; separator?: string },
) {
function whatsappAppLink(url: string, { message, phone, separator = ' ' }: WhatsappOptions) {
assert(url, 'whatsapp.url');
return (
'whatsapp://send' +
objectToGetParams({
phone: whatsappPhone(phone),
text: message ? message + separator + url : url,
})
);
}

export const WhatsappShareButton = forwardRef<HTMLButtonElement, WhatsappShareButtonProps>(
({ message, mobileBehavior, separator, ...props }, ref) => {
({ message, mobileBehavior, phone, separator, ...props }, ref) => {
const { linkBehavior, networkLink } = resolveMobileShareStrategy(
mobileBehavior,
buildWhatsappShareUrl,
Expand All @@ -58,6 +59,7 @@ export const WhatsappShareButton = forwardRef<HTMLButtonElement, WhatsappShareBu
networkLink={networkLink}
opts={{
message,
phone,
separator: separator || ' ',
}}
windowHeight={400}
Expand Down
Loading
Loading