Add dark mode option to in-app embed snippet dialog (#7377)
* add color theme selection * change chevron direction * change chevron direction again * rm unused ref
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import React, {memo, useRef, useState} from 'react'
|
import {memo, useEffect, useMemo, useState} from 'react'
|
||||||
import {TextInput, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -8,12 +8,19 @@ import {EMBED_SCRIPT} from '#/lib/constants'
|
|||||||
import {niceDate} from '#/lib/strings/time'
|
import {niceDate} from '#/lib/strings/time'
|
||||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
||||||
|
import {
|
||||||
|
ChevronBottom_Stroke2_Corner0_Rounded as ChevronBottomIcon,
|
||||||
|
ChevronRight_Stroke2_Corner0_Rounded as ChevronRightIcon,
|
||||||
|
} from '#/components/icons/Chevron'
|
||||||
|
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBracketsIcon} from '#/components/icons/CodeBrackets'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
|
||||||
|
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||||
|
|
||||||
type EmbedDialogProps = {
|
type EmbedDialogProps = {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
@@ -44,11 +51,12 @@ function EmbedDialogInner({
|
|||||||
}: Omit<EmbedDialogProps, 'control'>) {
|
}: Omit<EmbedDialogProps, 'control'>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_, i18n} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const ref = useRef<TextInput>(null)
|
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
const [showCustomisation, setShowCustomisation] = useState(false)
|
||||||
|
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||||
|
|
||||||
// reset copied state after 2 seconds
|
// reset copied state after 2 seconds
|
||||||
React.useEffect(() => {
|
useEffect(() => {
|
||||||
if (copied) {
|
if (copied) {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setCopied(false)
|
setCopied(false)
|
||||||
@@ -57,7 +65,7 @@ function EmbedDialogInner({
|
|||||||
}
|
}
|
||||||
}, [copied])
|
}, [copied])
|
||||||
|
|
||||||
const snippet = React.useMemo(() => {
|
const snippet = useMemo(() => {
|
||||||
function toEmbedUrl(href: string) {
|
function toEmbedUrl(href: string) {
|
||||||
return toShareUrl(href) + '?ref_src=embed'
|
return toShareUrl(href) + '?ref_src=embed'
|
||||||
}
|
}
|
||||||
@@ -75,9 +83,11 @@ function EmbedDialogInner({
|
|||||||
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
// x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x
|
||||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||||
postUri,
|
postUri,
|
||||||
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml(
|
)}" data-bluesky-cid="${escapeHtml(
|
||||||
lang,
|
postCid,
|
||||||
)}">${escapeHtml(record.text)}${
|
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||||
|
colorMode,
|
||||||
|
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||||
record.embed
|
record.embed
|
||||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||||
: ''
|
: ''
|
||||||
@@ -88,12 +98,13 @@ function EmbedDialogInner({
|
|||||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||||
niceDate(i18n, timestamp),
|
niceDate(i18n, timestamp),
|
||||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||||
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
|
}, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
<Dialog.Inner label="Embed post" style={[{maxWidth: 500}]}>
|
||||||
<View style={[a.gap_sm, a.pb_lg]}>
|
<View style={[a.gap_lg]}>
|
||||||
<Text style={[a.text_2xl, a.font_bold]}>
|
<View style={[a.gap_sm]}>
|
||||||
|
<Text style={[a.text_2xl, a.font_heavy]}>
|
||||||
<Trans>Embed post</Trans>
|
<Trans>Embed post</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
@@ -104,17 +115,73 @@ function EmbedDialogInner({
|
|||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.rounded_sm,
|
||||||
|
a.overflow_hidden,
|
||||||
|
]}>
|
||||||
|
<Button
|
||||||
|
label={
|
||||||
|
showCustomisation
|
||||||
|
? _(msg`Hide customization options`)
|
||||||
|
: _(msg`Show customization options`)
|
||||||
|
}
|
||||||
|
color="secondary"
|
||||||
|
variant="ghost"
|
||||||
|
size="small"
|
||||||
|
shape="default"
|
||||||
|
onPress={() => setShowCustomisation(c => !c)}
|
||||||
|
style={[
|
||||||
|
a.justify_start,
|
||||||
|
showCustomisation && t.atoms.bg_contrast_25,
|
||||||
|
]}>
|
||||||
|
<ButtonIcon
|
||||||
|
icon={showCustomisation ? ChevronBottomIcon : ChevronRightIcon}
|
||||||
|
/>
|
||||||
|
<ButtonText>
|
||||||
|
<Trans>Customization options</Trans>
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{showCustomisation && (
|
||||||
|
<View style={[a.gap_sm, a.p_md]}>
|
||||||
|
<Text style={[t.atoms.text_contrast_medium, a.font_bold]}>
|
||||||
|
<Trans>Color theme</Trans>
|
||||||
|
</Text>
|
||||||
|
<ToggleButton.Group
|
||||||
|
label={_(msg`Color mode`)}
|
||||||
|
values={[colorMode]}
|
||||||
|
onChange={([value]) => setColorMode(value as ColorModeValues)}>
|
||||||
|
<ToggleButton.Button name="system" label={_(msg`System`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>System</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="light" label={_(msg`Light`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>Light</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
<ToggleButton.Button name="dark" label={_(msg`Dark`)}>
|
||||||
|
<ToggleButton.ButtonText>
|
||||||
|
<Trans>Dark</Trans>
|
||||||
|
</ToggleButton.ButtonText>
|
||||||
|
</ToggleButton.Button>
|
||||||
|
</ToggleButton.Group>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
<View style={[a.flex_row, a.gap_sm]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
<TextField.Root>
|
<TextField.Root>
|
||||||
<TextField.Icon icon={CodeBrackets} />
|
<TextField.Icon icon={CodeBracketsIcon} />
|
||||||
<TextField.Input
|
<TextField.Input
|
||||||
label={_(msg`Embed HTML code`)}
|
label={_(msg`Embed HTML code`)}
|
||||||
editable={false}
|
editable={false}
|
||||||
selection={{start: 0, end: snippet.length}}
|
selection={{start: 0, end: snippet.length}}
|
||||||
value={snippet}
|
value={snippet}
|
||||||
style={{}}
|
|
||||||
/>
|
/>
|
||||||
</TextField.Root>
|
</TextField.Root>
|
||||||
</View>
|
</View>
|
||||||
@@ -124,14 +191,12 @@ function EmbedDialogInner({
|
|||||||
variant="solid"
|
variant="solid"
|
||||||
size="large"
|
size="large"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
ref.current?.focus()
|
|
||||||
ref.current?.setSelection(0, snippet.length)
|
|
||||||
navigator.clipboard.writeText(snippet)
|
navigator.clipboard.writeText(snippet)
|
||||||
setCopied(true)
|
setCopied(true)
|
||||||
}}>
|
}}>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<ButtonIcon icon={Check} />
|
<ButtonIcon icon={CheckIcon} />
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Copied!</Trans>
|
<Trans>Copied!</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
@@ -143,6 +208,7 @@ function EmbedDialogInner({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
</Dialog.Inner>
|
</Dialog.Inner>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user