Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b685f02a65 | |||
| 5568be4bc7 |
+1
-2
@@ -32,7 +32,6 @@ module.exports = {
|
||||
'H6',
|
||||
'P',
|
||||
'Admonition',
|
||||
'Admonition.Admonition',
|
||||
],
|
||||
impliedTextProps: [],
|
||||
suggestedTextWrappers: {
|
||||
@@ -92,9 +91,9 @@ module.exports = {
|
||||
'*.lock',
|
||||
'.husky',
|
||||
'patches',
|
||||
'bskyweb',
|
||||
'*.html',
|
||||
'bskyweb',
|
||||
'bskyembed',
|
||||
'src/locale/locales/_build/',
|
||||
'src/locale/locales/**/*.js',
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- name: Check for EXPO_TOKEN
|
||||
run: >
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
@@ -134,7 +134,7 @@ jobs:
|
||||
# GitHub actions are horrible so let's just copy paste this in
|
||||
buildIfNecessaryIOS:
|
||||
name: Build and Submit iOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-14
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}-build-ios
|
||||
cancel-in-progress: true
|
||||
@@ -176,7 +176,7 @@ jobs:
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: '16.2'
|
||||
xcode-version: '15.3'
|
||||
|
||||
- name: ☕️ Setup Cocoapods
|
||||
uses: maxim-lobanov/setup-cocoapods@v1
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
flows:
|
||||
- "flows/**"
|
||||
- "flows/*"
|
||||
@@ -17,7 +17,7 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "Post with an image"
|
||||
- tapOn:
|
||||
id: "openGalleryBtn"
|
||||
- tapOn: "Allow Full Access"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Content warnings"
|
||||
- tapOn: "Porn"
|
||||
- tapOn:
|
||||
|
||||
@@ -24,10 +24,12 @@ appId: xyz.blueskyweb.app
|
||||
- inputText: "example@test.com"
|
||||
- tapOn:
|
||||
id: "passwordInput"
|
||||
- inputText: "hunter22"
|
||||
- hideKeyboard
|
||||
- inputText: "hunter2"
|
||||
- pressKey: Enter
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
- tapOn:
|
||||
id: "handleInput"
|
||||
- inputText: "e2e-test"
|
||||
- tapOn:
|
||||
id: "nextBtn"
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: "?users&follows&posts"
|
||||
- runFlow:
|
||||
file: ../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn: "Create report for Misleading Post"
|
||||
- tapOn: "Send report to Dev-env Moderation"
|
||||
- tapOn:
|
||||
point: "50%,90%"
|
||||
@@ -1,24 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn: "Carla's avatar"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownBtn"
|
||||
- tapOn:
|
||||
id: "profileHeaderDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,24 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,35 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
# reason "other" defaults with details open
|
||||
- assertVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:clearOption"
|
||||
- assertNotVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonSpam"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -1,30 +0,0 @@
|
||||
appId: xyz.blueskyweb.app
|
||||
---
|
||||
- runScript:
|
||||
file: ../../setupServer.js
|
||||
env:
|
||||
SERVER_PATH: ?users&follows&posts&feeds
|
||||
- runFlow:
|
||||
file: ../../setupApp.yml
|
||||
- tapOn:
|
||||
id: "e2eSignInAlice"
|
||||
|
||||
- tapOn:
|
||||
id: "postDropdownBtn"
|
||||
index: 0
|
||||
- tapOn:
|
||||
id: "postDropdownReportBtn"
|
||||
- tapOn:
|
||||
id: "report:option:com.atproto.moderation.defs#reasonOther"
|
||||
- assertVisible:
|
||||
id: "report:labeler:mod-authority.test"
|
||||
# reason "other" defaults with details open
|
||||
- assertVisible:
|
||||
id: "report:details"
|
||||
- tapOn:
|
||||
id: "report:details"
|
||||
- inputText: "This is a test report"
|
||||
- tapOn:
|
||||
id: "report:submit"
|
||||
- assertNotVisible:
|
||||
id: "report:dialog"
|
||||
@@ -75,17 +75,14 @@ module.exports = function (config) {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'cy',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
'gd',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
@@ -240,7 +237,6 @@ module.exports = function (config) {
|
||||
},
|
||||
],
|
||||
'./plugins/starterPackAppClipExtension/withStarterPackAppClip.js',
|
||||
'./plugins/withGradleJVMHeapSizeIncrease.js',
|
||||
'./plugins/withAndroidManifestPlugin.js',
|
||||
'./plugins/withAndroidManifestFCMIconPlugin.js',
|
||||
'./plugins/withAndroidStylesAccentColorPlugin.js',
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z" clip-rule="evenodd"/></svg>
|
||||
|
Before Width: | Height: | Size: 454 B |
@@ -68,7 +68,6 @@ function scan(node = document) {
|
||||
if (ref_url.startsWith('http')) {
|
||||
searchParams.set('ref_url', encodeURIComponent(ref_url))
|
||||
}
|
||||
searchParams.set('colorMode', embed.dataset.blueskyColorMode || 'system')
|
||||
|
||||
const iframe = document.createElement('iframe')
|
||||
iframe.setAttribute('data-bluesky-id', id)
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||
|
||||
export function assertColorModeValues(value: string): value is ColorModeValues {
|
||||
return ['system', 'light', 'dark'].includes(value)
|
||||
}
|
||||
|
||||
export function applyTheme(theme: 'light' | 'dark') {
|
||||
document.documentElement.classList.remove('light', 'dark')
|
||||
document.documentElement.classList.add(theme)
|
||||
}
|
||||
|
||||
export function initSystemColorMode() {
|
||||
export function initColorMode() {
|
||||
applyTheme(
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
|
||||
@@ -9,14 +9,3 @@
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='black' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: calc(100% - 0.75rem) center;
|
||||
padding-right: 2rem;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' height='14px' width='14px' fill='none' viewBox='0 0 24 24'><path fill='white' fill-rule='evenodd' d='M3.293 8.293a1 1 0 0 1 1.414 0L12 15.586l7.293-7.293a1 1 0 1 1 1.414 1.414l-8 8a1 1 0 0 1-1.414 0l-8-8a1 1 0 0 1 0-1.414Z' clip-rule='evenodd'/></svg>");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import '../index.css'
|
||||
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtpAgent, AtUri} from '@atproto/api'
|
||||
import {AppBskyFeedDefs, AppBskyFeedPost, AtUri, BskyAgent} from '@atproto/api'
|
||||
import {h, render} from 'preact'
|
||||
import {useEffect, useMemo, useRef, useState} from 'preact/hooks'
|
||||
|
||||
import arrowBottom from '../../assets/arrowBottom_stroke2_corner0_rounded.svg'
|
||||
import logo from '../../assets/logo.svg'
|
||||
import {
|
||||
assertColorModeValues,
|
||||
ColorModeValues,
|
||||
initSystemColorMode,
|
||||
} from '../color-mode'
|
||||
import {initColorMode} from '../color-mode'
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -26,9 +22,9 @@ export const EMBED_SCRIPT = `${EMBED_SERVICE}/static/embed.js`
|
||||
const root = document.getElementById('app')
|
||||
if (!root) throw new Error('No root element')
|
||||
|
||||
initSystemColorMode()
|
||||
initColorMode()
|
||||
|
||||
const agent = new AtpAgent({
|
||||
const agent = new BskyAgent({
|
||||
service: 'https://public.api.bsky.app',
|
||||
})
|
||||
|
||||
@@ -36,7 +32,6 @@ render(<LandingPage />, root)
|
||||
|
||||
function LandingPage() {
|
||||
const [uri, setUri] = useState('')
|
||||
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
|
||||
@@ -125,50 +120,24 @@ function LandingPage() {
|
||||
|
||||
<h1 className="text-4xl font-bold text-center">Embed a Bluesky Post</h1>
|
||||
|
||||
<div className="flex flex-col w-full max-w-[600px] gap-6">
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
onInput={e => setUri(e.currentTarget.value)}
|
||||
className="border rounded-lg py-3 px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||
placeholder={DEFAULT_POST}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-sm font-medium" for="colorModeSelect">
|
||||
Theme
|
||||
</label>
|
||||
<select
|
||||
value={colorMode}
|
||||
onChange={e => {
|
||||
const value = e.currentTarget.value
|
||||
if (assertColorModeValues(value)) {
|
||||
setColorMode(value)
|
||||
}
|
||||
}}
|
||||
id="colorModeSelect"
|
||||
className="appearance-none bg-white border w-full rounded-lg text-sm px-3 py-2 dark:bg-dimmedBg dark:border-slate-500">
|
||||
<option value="system">System</option>
|
||||
<option value="light">Light</option>
|
||||
<option value="dark">Dark</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={uri}
|
||||
onInput={e => setUri(e.currentTarget.value)}
|
||||
className="border rounded-lg py-3 w-full max-w-[600px] px-4 dark:bg-dimmedBg dark:border-slate-500"
|
||||
placeholder={DEFAULT_POST}
|
||||
/>
|
||||
|
||||
<img src={arrowBottom} className="w-6 dark:invert" />
|
||||
|
||||
{loading ? (
|
||||
<div className={`${colorMode} w-full max-w-[600px]`}>
|
||||
<div className="w-full max-w-[600px]">
|
||||
<Skeleton />
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-[600px] gap-8 flex flex-col">
|
||||
{!error && thread && uri && (
|
||||
<Snippet thread={thread} colorMode={colorMode} />
|
||||
)}
|
||||
<div className={colorMode}>
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
</div>
|
||||
{!error && thread && uri && <Snippet thread={thread} />}
|
||||
{!error && thread && <Post thread={thread} key={thread.post.uri} />}
|
||||
{error && (
|
||||
<div className="w-full border border-red-500 bg-red-500/10 px-4 py-3 rounded-lg">
|
||||
<p className="text-red-500 text-center">{error}</p>
|
||||
@@ -199,13 +168,7 @@ function Skeleton() {
|
||||
)
|
||||
}
|
||||
|
||||
function Snippet({
|
||||
thread,
|
||||
colorMode,
|
||||
}: {
|
||||
thread: AppBskyFeedDefs.ThreadViewPost
|
||||
colorMode: ColorModeValues
|
||||
}) {
|
||||
function Snippet({thread}: {thread: AppBskyFeedDefs.ThreadViewPost}) {
|
||||
const ref = useRef<HTMLInputElement>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
@@ -241,11 +204,9 @@ function Snippet({
|
||||
// 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(
|
||||
thread.post.uri,
|
||||
)}" data-bluesky-cid="${escapeHtml(
|
||||
thread.post.cid,
|
||||
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||
colorMode,
|
||||
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||
)}" data-bluesky-cid="${escapeHtml(thread.post.cid)}"><p lang="${escapeHtml(
|
||||
lang,
|
||||
)}">${escapeHtml(record.text)}${
|
||||
record.embed
|
||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||
: ''
|
||||
@@ -256,7 +217,7 @@ function Snippet({
|
||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||
niceDate(thread.post.indexedAt),
|
||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||
}, [thread, colorMode])
|
||||
}, [thread])
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
|
||||
@@ -4,7 +4,7 @@ import {AppBskyFeedDefs, AtpAgent} from '@atproto/api'
|
||||
import {h, render} from 'preact'
|
||||
|
||||
import logo from '../../assets/logo.svg'
|
||||
import {applyTheme, initSystemColorMode} from '../color-mode'
|
||||
import {initColorMode} from '../color-mode'
|
||||
import {Container} from '../components/container'
|
||||
import {Link} from '../components/link'
|
||||
import {Post} from '../components/post'
|
||||
@@ -22,23 +22,7 @@ if (!uri) {
|
||||
throw new Error('No uri in path')
|
||||
}
|
||||
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
|
||||
// theme - default to light mode
|
||||
const colorMode = query.get('colorMode')
|
||||
|
||||
switch (colorMode) {
|
||||
case 'dark':
|
||||
applyTheme('dark')
|
||||
break
|
||||
case 'system':
|
||||
initSystemColorMode()
|
||||
break
|
||||
case 'light':
|
||||
default:
|
||||
applyTheme('light')
|
||||
break
|
||||
}
|
||||
initColorMode()
|
||||
|
||||
agent
|
||||
.getPostThread({
|
||||
|
||||
@@ -94,20 +94,6 @@ func run(args []string) {
|
||||
Value: "",
|
||||
EnvVars: []string{"STATIC_CDN_HOST"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "bsky-canonical-instance",
|
||||
Usage: "Enable if this is the canonical deployment (bsky.app)",
|
||||
Value: false,
|
||||
Required: false,
|
||||
EnvVars: []string{"BSKY_CANONICAL_INSTANCE"},
|
||||
},
|
||||
&cli.BoolFlag{
|
||||
Name: "robots-disallow-all",
|
||||
Usage: "Crawling is allowed by default. Enable this flag to Disallow all",
|
||||
Value: false,
|
||||
Required: false,
|
||||
EnvVars: []string{"ROBOTS_DISALLOW_ALL"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -63,8 +63,6 @@ func serve(cctx *cli.Context) error {
|
||||
corsOrigins := cctx.StringSlice("cors-allowed-origins")
|
||||
staticCDNHost := cctx.String("static-cdn-host")
|
||||
staticCDNHost = strings.TrimSuffix(staticCDNHost, "/")
|
||||
canonicalInstance := cctx.Bool("bsky-canonical-instance")
|
||||
robotsDisallowAll := cctx.Bool("robots-disallow-all")
|
||||
|
||||
// Echo
|
||||
e := echo.New()
|
||||
@@ -206,23 +204,13 @@ func serve(cctx *cli.Context) error {
|
||||
return http.FS(fsys)
|
||||
}())
|
||||
|
||||
// enable some special endpoints for the "canonical" deployment (bsky.app). not having these enabled should *not* impact regular operation
|
||||
if canonicalInstance {
|
||||
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
|
||||
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
|
||||
e.GET("/security.txt", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
||||
})
|
||||
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
|
||||
}
|
||||
|
||||
// default to permissive, but Disallow all if flag set
|
||||
if robotsDisallowAll {
|
||||
e.File("/robots.txt", "static/robots-disallow-all.txt")
|
||||
} else {
|
||||
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
||||
}
|
||||
|
||||
e.GET("/robots.txt", echo.WrapHandler(staticHandler))
|
||||
e.GET("/ips-v4", echo.WrapHandler(staticHandler))
|
||||
e.GET("/ips-v6", echo.WrapHandler(staticHandler))
|
||||
e.GET("/.well-known/*", echo.WrapHandler(staticHandler))
|
||||
e.GET("/security.txt", func(c echo.Context) error {
|
||||
return c.Redirect(http.StatusMovedPermanently, "/.well-known/security.txt")
|
||||
})
|
||||
e.GET("/iframe/youtube.html", echo.WrapHandler(staticHandler))
|
||||
e.GET("/static/*", echo.WrapHandler(http.StripPrefix("/static/", staticHandler)), func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# This is an development or self-hosted instance of the bsky web app, and crawling has been disallowed by the operator team.
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
@@ -5,18 +5,15 @@ module.exports = {
|
||||
'an',
|
||||
'ast',
|
||||
'ca',
|
||||
'cy',
|
||||
'da',
|
||||
'de',
|
||||
'el',
|
||||
'en-GB',
|
||||
'eo',
|
||||
'es',
|
||||
'eu',
|
||||
'fi',
|
||||
'fr',
|
||||
'ga',
|
||||
'gd',
|
||||
'gl',
|
||||
'hi',
|
||||
'hu',
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bsky.app",
|
||||
"version": "1.99.1",
|
||||
"version": "1.99.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -57,7 +57,7 @@
|
||||
"icons:optimize": "svgo -f ./assets/icons"
|
||||
},
|
||||
"dependencies": {
|
||||
"@atproto/api": "^0.14.7",
|
||||
"@atproto/api": "^0.14.0",
|
||||
"@bitdrift/react-native": "^0.6.8",
|
||||
"@braintree/sanitize-url": "^6.0.2",
|
||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
const {withGradleProperties} = require('expo/config-plugins')
|
||||
|
||||
function setGradlePropertiesValue(config, key, value) {
|
||||
return withGradleProperties(config, exportedConfig => {
|
||||
const keyIdx = exportedConfig.modResults.findIndex(
|
||||
item => item.type === 'property' && item.key === key,
|
||||
)
|
||||
if (keyIdx >= 0) {
|
||||
exportedConfig.modResults.splice(keyIdx, 1, {
|
||||
type: 'property',
|
||||
key,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
exportedConfig.modResults.push({
|
||||
type: 'property',
|
||||
key,
|
||||
value,
|
||||
})
|
||||
}
|
||||
|
||||
return exportedConfig
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = function withGradleJVMHeapSizeIncrease(config) {
|
||||
config = setGradlePropertiesValue(
|
||||
config,
|
||||
'org.gradle.jvmargs',
|
||||
'-Xmx4096m -XX:MaxMetaspaceSize=1024m', //Set data of your choice
|
||||
)
|
||||
|
||||
return config
|
||||
}
|
||||
+2
-9
@@ -32,7 +32,6 @@ import {
|
||||
import {RouteParams, State} from '#/lib/routes/types'
|
||||
import {attachRouteToLogEvents, logEvent} from '#/lib/statsig/statsig'
|
||||
import {bskyTitle} from '#/lib/strings/headings'
|
||||
import {logger} from '#/logger'
|
||||
import {isNative, isWeb} from '#/platform/detection'
|
||||
import {useModalControls} from '#/state/modals'
|
||||
import {useUnreadNotifications} from '#/state/queries/notifications/unread'
|
||||
@@ -69,7 +68,6 @@ import {SharedPreferencesTesterScreen} from '#/screens/E2E/SharedPreferencesTest
|
||||
import HashtagScreen from '#/screens/Hashtag'
|
||||
import {MessagesScreen} from '#/screens/Messages/ChatList'
|
||||
import {MessagesConversationScreen} from '#/screens/Messages/Conversation'
|
||||
import {MessagesInboxScreen} from '#/screens/Messages/Inbox'
|
||||
import {MessagesSettingsScreen} from '#/screens/Messages/Settings'
|
||||
import {ModerationScreen} from '#/screens/Moderation'
|
||||
import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings'
|
||||
@@ -412,11 +410,6 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
getComponent={() => MessagesSettingsScreen}
|
||||
options={{title: title(msg`Chat settings`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="MessagesInbox"
|
||||
getComponent={() => MessagesInboxScreen}
|
||||
options={{title: title(msg`Chat request inbox`), requireAuth: true}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NotificationSettings"
|
||||
getComponent={() => NotificationSettingsScreen}
|
||||
@@ -736,7 +729,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
linking={LINKING}
|
||||
theme={theme}
|
||||
onStateChange={() => {
|
||||
logger.metric('router:navigate', {
|
||||
logEvent('lake:router:navigate', {
|
||||
from: prevLoggedRouteName.current,
|
||||
})
|
||||
prevLoggedRouteName.current = getCurrentRouteName()
|
||||
@@ -745,7 +738,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
attachRouteToLogEvents(getCurrentRouteName)
|
||||
logModuleInitTime()
|
||||
onReady()
|
||||
logger.metric('router:navigate', {})
|
||||
logEvent('lake:router:navigate', {})
|
||||
}}>
|
||||
{children}
|
||||
</NavigationContainer>
|
||||
|
||||
+1
-22
@@ -31,18 +31,6 @@ export const atoms = {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
top_0: {
|
||||
top: 0,
|
||||
},
|
||||
right_0: {
|
||||
right: 0,
|
||||
},
|
||||
bottom_0: {
|
||||
bottom: 0,
|
||||
},
|
||||
left_0: {
|
||||
left: 0,
|
||||
},
|
||||
z_10: {
|
||||
zIndex: 10,
|
||||
},
|
||||
@@ -70,7 +58,7 @@ export const atoms = {
|
||||
}),
|
||||
|
||||
/*
|
||||
* Width & Height
|
||||
* Width
|
||||
*/
|
||||
w_full: {
|
||||
width: '100%',
|
||||
@@ -81,12 +69,6 @@ export const atoms = {
|
||||
h_full_vh: web({
|
||||
height: '100vh',
|
||||
}),
|
||||
max_w_full: {
|
||||
maxWidth: '100%',
|
||||
},
|
||||
max_h_full: {
|
||||
maxHeight: '100%',
|
||||
},
|
||||
|
||||
/**
|
||||
* Used for the outermost components on screens, to ensure that they can fill
|
||||
@@ -111,9 +93,6 @@ export const atoms = {
|
||||
/*
|
||||
* Border radius
|
||||
*/
|
||||
rounded_0: {
|
||||
borderRadius: 0,
|
||||
},
|
||||
rounded_2xs: {
|
||||
borderRadius: tokens.borderRadius._2xs,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,6 @@ import React from 'react'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {Button as BaseButton, ButtonProps} from '#/components/Button'
|
||||
import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo'
|
||||
import {Eye_Stroke2_Corner0_Rounded as InfoIcon} from '#/components/icons/Eye'
|
||||
import {Leaf_Stroke2_Corner0_Rounded as TipIcon} from '#/components/icons/Leaf'
|
||||
@@ -50,29 +49,22 @@ export function Text({
|
||||
return (
|
||||
<BaseText
|
||||
{...rest}
|
||||
style={[a.flex_1, a.text_sm, a.leading_snug, a.pr_md, style]}>
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
{
|
||||
paddingTop: 1,
|
||||
},
|
||||
style,
|
||||
]}>
|
||||
{children}
|
||||
</BaseText>
|
||||
)
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
...props
|
||||
}: Omit<ButtonProps, 'size' | 'variant' | 'color'>) {
|
||||
return (
|
||||
<BaseButton size="tiny" variant="outline" color="secondary" {...props}>
|
||||
{children}
|
||||
</BaseButton>
|
||||
)
|
||||
}
|
||||
|
||||
export function Row({children}: {children: React.ReactNode}) {
|
||||
return (
|
||||
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
|
||||
{children}
|
||||
</View>
|
||||
)
|
||||
return <View style={[a.flex_row, a.gap_sm]}>{children}</View>
|
||||
}
|
||||
|
||||
export function Outer({
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
import {View} from 'react-native'
|
||||
import {moderateProfile} from '@atproto/api'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfilesQuery} from '#/state/queries/profile'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function AvatarStack({
|
||||
profiles,
|
||||
size = 26,
|
||||
numPending,
|
||||
backgroundColor,
|
||||
}: {
|
||||
profiles: bsky.profile.AnyProfileView[]
|
||||
profiles: string[]
|
||||
size?: number
|
||||
numPending?: number
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
const halfSize = size / 2
|
||||
const {data, error} = useProfilesQuery({handles: profiles})
|
||||
const t = useTheme()
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const isPending = (numPending && profiles.length === 0) || !moderationOpts
|
||||
if (error) {
|
||||
console.error(error)
|
||||
return null
|
||||
}
|
||||
|
||||
const isPending = !data || !moderationOpts
|
||||
|
||||
const items = isPending
|
||||
? Array.from({length: numPending ?? profiles.length}).map((_, i) => ({
|
||||
? Array.from({length: profiles.length}).map((_, i) => ({
|
||||
key: i,
|
||||
profile: null,
|
||||
moderation: null,
|
||||
}))
|
||||
: profiles.map(item => ({
|
||||
: data.profiles.map(item => ({
|
||||
key: item.did,
|
||||
profile: item,
|
||||
moderation: moderateProfile(item, moderationOpts),
|
||||
@@ -56,7 +56,7 @@ export function AvatarStack({
|
||||
height: size,
|
||||
left: i * -halfSize,
|
||||
borderWidth: 1,
|
||||
borderColor: backgroundColor ?? t.atoms.bg.backgroundColor,
|
||||
borderColor: t.atoms.bg.backgroundColor,
|
||||
borderRadius: 999,
|
||||
zIndex: 3 - i,
|
||||
},
|
||||
@@ -74,33 +74,3 @@ export function AvatarStack({
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export function AvatarStackWithFetch({
|
||||
profiles,
|
||||
size,
|
||||
backgroundColor,
|
||||
}: {
|
||||
profiles: string[]
|
||||
size?: number
|
||||
backgroundColor?: string
|
||||
}) {
|
||||
const {data, error} = useProfilesQuery({handles: profiles})
|
||||
|
||||
if (error) {
|
||||
if (error.name !== 'AbortError') {
|
||||
logger.error('Error fetching profiles for AvatarStack', {
|
||||
safeMessage: error,
|
||||
})
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<AvatarStack
|
||||
numPending={profiles.length}
|
||||
profiles={data?.profiles || []}
|
||||
size={size}
|
||||
backgroundColor={backgroundColor}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,10 +68,7 @@ export type DialogOuterProps = {
|
||||
testID?: string
|
||||
}
|
||||
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> &
|
||||
T & {
|
||||
testID?: string
|
||||
}
|
||||
type DialogInnerPropsBase<T> = React.PropsWithChildren<ViewStyleProp> & T
|
||||
export type DialogInnerProps =
|
||||
| DialogInnerPropsBase<{
|
||||
label?: undefined
|
||||
|
||||
@@ -269,7 +269,7 @@ function SaveButtonInner({
|
||||
},
|
||||
])
|
||||
}
|
||||
Toast.show(_(msg({message: 'Feeds updated!', context: 'toast'})))
|
||||
Toast.show(_(msg`Feeds updated!`))
|
||||
} catch (err: any) {
|
||||
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
|
||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||
|
||||
@@ -33,13 +33,11 @@ export function KnownFollowers({
|
||||
moderationOpts,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
showIfEmpty,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
}) {
|
||||
const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(
|
||||
new Map(),
|
||||
@@ -66,12 +64,11 @@ export function KnownFollowers({
|
||||
moderationOpts={moderationOpts}
|
||||
onLinkPress={onLinkPress}
|
||||
minimal={minimal}
|
||||
showIfEmpty={showIfEmpty}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <EmptyFallback show={showIfEmpty} />
|
||||
return null
|
||||
}
|
||||
|
||||
function KnownFollowersInner({
|
||||
@@ -80,19 +77,22 @@ function KnownFollowersInner({
|
||||
cachedKnownFollowers,
|
||||
onLinkPress,
|
||||
minimal,
|
||||
showIfEmpty,
|
||||
}: {
|
||||
profile: bsky.profile.AnyProfileView
|
||||
moderationOpts: ModerationOpts
|
||||
cachedKnownFollowers: AppBskyActorDefs.KnownFollowers
|
||||
onLinkPress?: LinkProps['onPress']
|
||||
minimal?: boolean
|
||||
showIfEmpty?: boolean
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
|
||||
const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]
|
||||
const textStyle = [
|
||||
a.flex_1,
|
||||
a.text_sm,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]
|
||||
|
||||
const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => {
|
||||
const moderation = moderateProfile(f, moderationOpts)
|
||||
@@ -115,7 +115,7 @@ function KnownFollowersInner({
|
||||
* We check above too, but here for clarity and a reminder to _check for
|
||||
* valid indices_
|
||||
*/
|
||||
if (slice.length === 0) return <EmptyFallback show={showIfEmpty} />
|
||||
if (slice.length === 0) return null
|
||||
|
||||
const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE
|
||||
|
||||
@@ -127,7 +127,7 @@ function KnownFollowersInner({
|
||||
onPress={onLinkPress}
|
||||
to={makeProfileLink(profile, 'known-followers')}
|
||||
style={[
|
||||
a.max_w_full,
|
||||
a.flex_1,
|
||||
a.flex_row,
|
||||
minimal ? a.gap_sm : a.gap_md,
|
||||
a.align_center,
|
||||
@@ -172,7 +172,6 @@ function KnownFollowersInner({
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.flex_shrink,
|
||||
textStyle,
|
||||
hovered && {
|
||||
textDecorationLine: 'underline',
|
||||
@@ -244,15 +243,3 @@ function KnownFollowersInner({
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
function EmptyFallback({show}: {show?: boolean}) {
|
||||
const t = useTheme()
|
||||
|
||||
if (!show) return null
|
||||
|
||||
return (
|
||||
<Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
<Trans>Not followed by anyone you're following</Trans>
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {memo, useEffect, useMemo, useState} from 'react'
|
||||
import {View} from 'react-native'
|
||||
import React, {memo, useRef, useState} from 'react'
|
||||
import {TextInput, View} from 'react-native'
|
||||
import {AppBskyActorDefs, AppBskyFeedPost, AtUri} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,19 +8,12 @@ import {EMBED_SCRIPT} from '#/lib/constants'
|
||||
import {niceDate} from '#/lib/strings/time'
|
||||
import {toShareUrl} from '#/lib/strings/url-helpers'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import * as TextField from '#/components/forms/TextField'
|
||||
import * as ToggleButton from '#/components/forms/ToggleButton'
|
||||
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 {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
|
||||
import {CodeBrackets_Stroke2_Corner0_Rounded as CodeBrackets} from '#/components/icons/CodeBrackets'
|
||||
import {Text} from '#/components/Typography'
|
||||
|
||||
export type ColorModeValues = 'system' | 'light' | 'dark'
|
||||
import {Button, ButtonIcon, ButtonText} from '../Button'
|
||||
|
||||
type EmbedDialogProps = {
|
||||
control: Dialog.DialogControlProps
|
||||
@@ -51,12 +44,11 @@ function EmbedDialogInner({
|
||||
}: Omit<EmbedDialogProps, 'control'>) {
|
||||
const t = useTheme()
|
||||
const {_, i18n} = useLingui()
|
||||
const ref = useRef<TextInput>(null)
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [showCustomisation, setShowCustomisation] = useState(false)
|
||||
const [colorMode, setColorMode] = useState<ColorModeValues>('system')
|
||||
|
||||
// reset copied state after 2 seconds
|
||||
useEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (copied) {
|
||||
const timeout = setTimeout(() => {
|
||||
setCopied(false)
|
||||
@@ -65,7 +57,7 @@ function EmbedDialogInner({
|
||||
}
|
||||
}, [copied])
|
||||
|
||||
const snippet = useMemo(() => {
|
||||
const snippet = React.useMemo(() => {
|
||||
function toEmbedUrl(href: string) {
|
||||
return toShareUrl(href) + '?ref_src=embed'
|
||||
}
|
||||
@@ -83,11 +75,9 @@ 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
|
||||
return `<blockquote class="bluesky-embed" data-bluesky-uri="${escapeHtml(
|
||||
postUri,
|
||||
)}" data-bluesky-cid="${escapeHtml(
|
||||
postCid,
|
||||
)}" data-bluesky-embed-color-mode="${escapeHtml(
|
||||
colorMode,
|
||||
)}"><p lang="${escapeHtml(lang)}">${escapeHtml(record.text)}${
|
||||
)}" data-bluesky-cid="${escapeHtml(postCid)}"><p lang="${escapeHtml(
|
||||
lang,
|
||||
)}">${escapeHtml(record.text)}${
|
||||
record.embed
|
||||
? `<br><br><a href="${escapeHtml(href)}">[image or embed]</a>`
|
||||
: ''
|
||||
@@ -98,116 +88,60 @@ function EmbedDialogInner({
|
||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||
niceDate(i18n, timestamp),
|
||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||
}, [i18n, postUri, postCid, record, timestamp, postAuthor, colorMode])
|
||||
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
|
||||
|
||||
return (
|
||||
<Dialog.Inner label={_(msg`Embed post`)} style={[{maxWidth: 500}]}>
|
||||
<View style={[a.gap_lg]}>
|
||||
<View style={[a.gap_sm]}>
|
||||
<Text style={[a.text_2xl, a.font_heavy]}>
|
||||
<Trans>Embed post</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
|
||||
<Trans>
|
||||
Embed this post in your website. Simply copy the following snippet
|
||||
and paste it into the HTML code of your website.
|
||||
</Trans>
|
||||
</Text>
|
||||
</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>
|
||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
||||
<View style={[a.gap_sm, a.pb_lg]}>
|
||||
<Text style={[a.text_2xl, a.font_bold]}>
|
||||
<Trans>Embed post</Trans>
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_md, t.atoms.text_contrast_medium, a.leading_normal]}>
|
||||
<Trans>
|
||||
Embed this post in your website. Simply copy the following snippet
|
||||
and paste it into the HTML code of your website.
|
||||
</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{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 style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={CodeBrackets} />
|
||||
<TextField.Input
|
||||
label={_(msg`Embed HTML code`)}
|
||||
editable={false}
|
||||
selection={{start: 0, end: snippet.length}}
|
||||
value={snippet}
|
||||
style={{}}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
<View style={[a.flex_row, a.gap_sm]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<TextField.Root>
|
||||
<TextField.Icon icon={CodeBracketsIcon} />
|
||||
<TextField.Input
|
||||
label={_(msg`Embed HTML code`)}
|
||||
editable={false}
|
||||
selection={{start: 0, end: snippet.length}}
|
||||
value={snippet}
|
||||
/>
|
||||
</TextField.Root>
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Copy code`)}
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
navigator.clipboard.writeText(snippet)
|
||||
setCopied(true)
|
||||
}}>
|
||||
{copied ? (
|
||||
<>
|
||||
<ButtonIcon icon={CheckIcon} />
|
||||
<ButtonText>
|
||||
<Trans>Copied!</Trans>
|
||||
</ButtonText>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
label={_(msg`Copy code`)}
|
||||
color="primary"
|
||||
variant="solid"
|
||||
size="large"
|
||||
onPress={() => {
|
||||
ref.current?.focus()
|
||||
ref.current?.setSelection(0, snippet.length)
|
||||
navigator.clipboard.writeText(snippet)
|
||||
setCopied(true)
|
||||
}}>
|
||||
{copied ? (
|
||||
<>
|
||||
<ButtonIcon icon={Check} />
|
||||
<ButtonText>
|
||||
<Trans>Copy code</Trans>
|
||||
<Trans>Copied!</Trans>
|
||||
</ButtonText>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<ButtonText>
|
||||
<Trans>Copy code</Trans>
|
||||
</ButtonText>
|
||||
)}
|
||||
</Button>
|
||||
</View>
|
||||
<Dialog.Close />
|
||||
</Dialog.Inner>
|
||||
|
||||
@@ -173,9 +173,9 @@ function MenuContent({
|
||||
const {mutate: muteConvo} = useMuteConvo(convoId, {
|
||||
onSuccess: data => {
|
||||
if (data.convo.muted) {
|
||||
Toast.show(_(msg({message: 'Chat muted', context: 'toast'})))
|
||||
Toast.show(_(msg`Chat muted`))
|
||||
} else {
|
||||
Toast.show(_(msg({message: 'Chat unmuted', context: 'toast'})))
|
||||
Toast.show(_(msg`Chat unmuted`))
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
|
||||
@@ -13,12 +13,10 @@ export function LeaveConvoPrompt({
|
||||
control,
|
||||
convoId,
|
||||
currentScreen,
|
||||
hasMessages = true,
|
||||
}: {
|
||||
control: DialogOuterProps['control']
|
||||
convoId: string
|
||||
currentScreen: 'list' | 'conversation'
|
||||
hasMessages?: boolean
|
||||
}) {
|
||||
const {_} = useLingui()
|
||||
const navigation = useNavigation<NavigationProp>()
|
||||
@@ -41,9 +39,7 @@ export function LeaveConvoPrompt({
|
||||
control={control}
|
||||
title={_(msg`Leave conversation`)}
|
||||
description={_(
|
||||
hasMessages
|
||||
? msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`
|
||||
: msg`Are you sure you want to leave this conversation?`,
|
||||
msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`,
|
||||
)}
|
||||
confirmButtonCta={_(msg`Leave`)}
|
||||
confirmButtonColor="negative"
|
||||
|
||||
@@ -69,9 +69,7 @@ export let MessageMenu = ({
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
||||
convo
|
||||
.deleteMessage(message.id)
|
||||
.then(() =>
|
||||
Toast.show(_(msg({message: 'Message deleted', context: 'toast'}))),
|
||||
)
|
||||
.then(() => Toast.show(_(msg`Message deleted`)))
|
||||
.catch(() => Toast.show(_(msg`Failed to delete message`)))
|
||||
}, [_, convo, message.id])
|
||||
|
||||
|
||||
@@ -8,15 +8,13 @@ import {useNavigation} from '@react-navigation/native'
|
||||
import {useEmail} from '#/lib/hooks/useEmail'
|
||||
import {NavigationProp} from '#/lib/routes/types'
|
||||
import {logEvent} from '#/lib/statsig/statsig'
|
||||
import {useGetConvoAvailabilityQuery} from '#/state/queries/messages/get-convo-availability'
|
||||
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
import {useMaybeConvoForUser} from '#/state/queries/messages/get-convo-for-members'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {useDialogControl} from '#/components/Dialog'
|
||||
import {VerifyEmailDialog} from '#/components/dialogs/VerifyEmailDialog'
|
||||
import {canBeMessaged} from '#/components/dms/util'
|
||||
import {Message_Stroke2_Corner0_Rounded as Message} from '#/components/icons/Message'
|
||||
import {useDialogControl} from '../Dialog'
|
||||
import {VerifyEmailDialog} from '../dialogs/VerifyEmailDialog'
|
||||
|
||||
export function MessageProfileButton({
|
||||
profile,
|
||||
@@ -29,19 +27,10 @@ export function MessageProfileButton({
|
||||
const {needsEmailVerification} = useEmail()
|
||||
const verifyEmailControl = useDialogControl()
|
||||
|
||||
const {data: convoAvailability} = useGetConvoAvailabilityQuery(profile.did)
|
||||
const {mutate: initiateConvo} = useGetConvoForMembers({
|
||||
onSuccess: ({convo}) => {
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
||||
},
|
||||
onError: () => {
|
||||
Toast.show(_(msg`Failed to create conversation`))
|
||||
},
|
||||
})
|
||||
const {data: convo, isPending} = useMaybeConvoForUser(profile.did)
|
||||
|
||||
const onPress = React.useCallback(() => {
|
||||
if (!convoAvailability?.canChat) {
|
||||
if (!convo?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -50,25 +39,15 @@ export function MessageProfileButton({
|
||||
return
|
||||
}
|
||||
|
||||
if (convoAvailability.convo) {
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
navigation.navigate('MessagesConversation', {
|
||||
conversation: convoAvailability.convo.id,
|
||||
})
|
||||
} else {
|
||||
if (convo && !convo.lastMessage) {
|
||||
logEvent('chat:create', {logContext: 'ProfileHeader'})
|
||||
initiateConvo([profile.did])
|
||||
}
|
||||
}, [
|
||||
needsEmailVerification,
|
||||
verifyEmailControl,
|
||||
navigation,
|
||||
profile.did,
|
||||
initiateConvo,
|
||||
convoAvailability,
|
||||
])
|
||||
logEvent('chat:open', {logContext: 'ProfileHeader'})
|
||||
|
||||
if (!convoAvailability) {
|
||||
navigation.navigate('MessagesConversation', {conversation: convo.id})
|
||||
}, [needsEmailVerification, verifyEmailControl, convo, navigation])
|
||||
|
||||
if (isPending) {
|
||||
// show pending state based on declaration
|
||||
if (canBeMessaged(profile)) {
|
||||
return (
|
||||
@@ -90,7 +69,7 @@ export function MessageProfileButton({
|
||||
}
|
||||
}
|
||||
|
||||
if (convoAvailability.canChat) {
|
||||
if (convo) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -53,10 +53,10 @@ export let MessagesListHeader = ({
|
||||
}, [moderation])
|
||||
|
||||
const onPressBack = useCallback(() => {
|
||||
if (navigation.canGoBack()) {
|
||||
navigation.goBack()
|
||||
if (isWeb) {
|
||||
navigation.replace('Messages', {})
|
||||
} else {
|
||||
navigation.navigate('Messages', {})
|
||||
navigation.goBack()
|
||||
}
|
||||
}, [navigation])
|
||||
|
||||
|
||||
@@ -311,19 +311,6 @@ function DoneStep({
|
||||
},
|
||||
})
|
||||
|
||||
let btnText = _(msg`Done`)
|
||||
let toastMsg: string | undefined
|
||||
if (actions.includes('leave') && actions.includes('block')) {
|
||||
btnText = _(msg`Block and Delete`)
|
||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||
} else if (actions.includes('leave')) {
|
||||
btnText = _(msg`Delete Conversation`)
|
||||
toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'}))
|
||||
} else if (actions.includes('block')) {
|
||||
btnText = _(msg`Block User`)
|
||||
toastMsg = _(msg({message: 'User blocked', context: 'toast'}))
|
||||
}
|
||||
|
||||
const onPressPrimaryAction = () => {
|
||||
control.close(() => {
|
||||
if (actions.includes('block')) {
|
||||
@@ -332,12 +319,18 @@ function DoneStep({
|
||||
if (actions.includes('leave')) {
|
||||
leaveConvo()
|
||||
}
|
||||
if (toastMsg) {
|
||||
Toast.show(toastMsg, 'check')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let btnText = _(msg`Done`)
|
||||
if (actions.includes('leave') && actions.includes('block')) {
|
||||
btnText = _(msg`Block and Delete`)
|
||||
} else if (actions.includes('leave')) {
|
||||
btnText = _(msg`Delete Conversation`)
|
||||
} else if (actions.includes('block')) {
|
||||
btnText = _(msg`Block User`)
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={a.gap_2xl}>
|
||||
<View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
|
||||
|
||||
@@ -77,10 +77,7 @@ export function SearchablePeopleList({
|
||||
isFetching,
|
||||
} = useActorAutocompleteQuery(searchText, true, 12)
|
||||
const {data: follows} = useProfileFollowsQuery(currentAccount?.did)
|
||||
const {data: convos} = useListConvosQuery({
|
||||
enabled: showRecentConvos,
|
||||
status: 'accepted',
|
||||
})
|
||||
const {data: convos} = useListConvosQuery({enabled: showRecentConvos})
|
||||
|
||||
const items = useMemo(() => {
|
||||
let _items: Item[] = []
|
||||
|
||||
@@ -41,27 +41,25 @@ export function HostingProvider({
|
||||
onSelect={onSelectServiceUrl}
|
||||
/>
|
||||
{minimal ? (
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap, a.gap_xs]}>
|
||||
<View style={[a.flex_row, a.align_center, a.flex_wrap]}>
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
<Trans>You are creating an account on</Trans>
|
||||
<Trans>
|
||||
You are creating an account on{' '}
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Changes hosting provider`)}
|
||||
onPress={onPressSelectService}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[a.px_xs, {marginHorizontal: tokens.space.xs * -1}]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={PencilIcon} />
|
||||
</Button>
|
||||
</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
label={toNiceDomain(serviceUrl)}
|
||||
accessibilityHint={_(msg`Changes hosting provider`)}
|
||||
onPress={onPressSelectService}
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
size="tiny"
|
||||
style={[
|
||||
a.px_xs,
|
||||
{marginHorizontal: tokens.space.xs * -1},
|
||||
{paddingVertical: 0},
|
||||
]}>
|
||||
<ButtonText style={[a.text_sm]}>
|
||||
{toNiceDomain(serviceUrl)}
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={PencilIcon} />
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<Button
|
||||
|
||||
@@ -11,17 +11,14 @@ import React from 'react'
|
||||
import {formatDistance, Locale} from 'date-fns'
|
||||
import {
|
||||
ca,
|
||||
cy,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
enGB,
|
||||
eo,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
gd,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
@@ -56,18 +53,15 @@ const locales: Record<AppLanguage, Locale | undefined> = {
|
||||
an: undefined,
|
||||
ast: undefined,
|
||||
ca,
|
||||
cy,
|
||||
da,
|
||||
de,
|
||||
el,
|
||||
['en-GB']: enGB,
|
||||
eo,
|
||||
es,
|
||||
eu,
|
||||
fi,
|
||||
fr,
|
||||
ga: undefined,
|
||||
gd,
|
||||
gl,
|
||||
hi,
|
||||
hu,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createSinglePathSVG} from './TEMPLATE'
|
||||
|
||||
export const CircleX_Stroke2_Corner0_Rounded = createSinglePathSVG({
|
||||
path: 'M12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16ZM2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm6.293-3.707a1 1 0 0 1 1.414 0L12 10.586l2.293-2.293a1 1 0 1 1 1.414 1.414L13.414 12l2.293 2.293a1 1 0 0 1-1.414 1.414L12 13.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L10.586 12 8.293 9.707a1 1 0 0 1 0-1.414Z',
|
||||
})
|
||||
@@ -255,7 +255,7 @@ function AppealForm({
|
||||
},
|
||||
onSuccess: () => {
|
||||
control.close()
|
||||
Toast.show(_(msg({message: 'Appeal submitted', context: 'toast'})))
|
||||
Toast.show(_(msg`Appeal submitted`))
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import {
|
||||
$Typed,
|
||||
ChatBskyConvoDefs,
|
||||
ComAtprotoModerationCreateReport,
|
||||
} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useMutation} from '@tanstack/react-query'
|
||||
|
||||
import {logger} from '#/logger'
|
||||
import {useAgent} from '#/state/session'
|
||||
import {ReportState} from './state'
|
||||
import {ParsedReportSubject} from './types'
|
||||
|
||||
export function useSubmitReportMutation() {
|
||||
const {_} = useLingui()
|
||||
const agent = useAgent()
|
||||
|
||||
return useMutation({
|
||||
async mutationFn({
|
||||
subject,
|
||||
state,
|
||||
}: {
|
||||
subject: ParsedReportSubject
|
||||
state: ReportState
|
||||
}) {
|
||||
if (!state.selectedOption) {
|
||||
throw new Error(_(msg`Please select a reason for this report`))
|
||||
}
|
||||
if (!state.selectedLabeler) {
|
||||
throw new Error(_(msg`Please select a moderation service`))
|
||||
}
|
||||
|
||||
let report:
|
||||
| ComAtprotoModerationCreateReport.InputSchema
|
||||
| (Omit<ComAtprotoModerationCreateReport.InputSchema, 'subject'> & {
|
||||
subject: $Typed<ChatBskyConvoDefs.MessageRef>
|
||||
})
|
||||
|
||||
switch (subject.type) {
|
||||
case 'account': {
|
||||
report = {
|
||||
reasonType: state.selectedOption.reason,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'com.atproto.admin.defs#repoRef',
|
||||
did: subject.did,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'post':
|
||||
case 'list':
|
||||
case 'feed':
|
||||
case 'starterPack': {
|
||||
report = {
|
||||
reasonType: state.selectedOption.reason,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'com.atproto.repo.strongRef',
|
||||
uri: subject.uri,
|
||||
cid: subject.cid,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'chatMessage': {
|
||||
report = {
|
||||
reasonType: state.selectedOption.reason,
|
||||
reason: state.details,
|
||||
subject: {
|
||||
$type: 'chat.bsky.convo.defs#messageRef',
|
||||
messageId: subject.message.id,
|
||||
convoId: subject.convoId,
|
||||
did: subject.message.sender.did,
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (__DEV__) {
|
||||
logger.info('Submitting report', {
|
||||
labeler: {
|
||||
handle: state.selectedLabeler.creator.handle,
|
||||
},
|
||||
report,
|
||||
})
|
||||
} else {
|
||||
await agent.createModerationReport(report, {
|
||||
encoding: 'application/json',
|
||||
headers: {
|
||||
'atproto-proxy': `${state.selectedLabeler.creator.did}#atproto_labeler`,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export const DMCA_LINK = 'https://bsky.social/about/support/copyright'
|
||||
@@ -1,49 +0,0 @@
|
||||
import {useMemo} from 'react'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {ParsedReportSubject} from './types'
|
||||
|
||||
export function useCopyForSubject(subject: ParsedReportSubject) {
|
||||
const {_} = useLingui()
|
||||
return useMemo(() => {
|
||||
switch (subject.type) {
|
||||
case 'account': {
|
||||
return {
|
||||
title: _(msg`Report this user`),
|
||||
subtitle: _(msg`Why should this user be reviewed?`),
|
||||
}
|
||||
}
|
||||
case 'post': {
|
||||
return {
|
||||
title: _(msg`Report this post`),
|
||||
subtitle: _(msg`Why should this post be reviewed?`),
|
||||
}
|
||||
}
|
||||
case 'list': {
|
||||
return {
|
||||
title: _(msg`Report this list`),
|
||||
subtitle: _(msg`Why should this list be reviewed?`),
|
||||
}
|
||||
}
|
||||
case 'feed': {
|
||||
return {
|
||||
title: _(msg`Report this feed`),
|
||||
subtitle: _(msg`Why should this feed be reviewed?`),
|
||||
}
|
||||
}
|
||||
case 'starterPack': {
|
||||
return {
|
||||
title: _(msg`Report this starter pack`),
|
||||
subtitle: _(msg`Why should this starter pack be reviewed?`),
|
||||
}
|
||||
}
|
||||
case 'chatMessage': {
|
||||
return {
|
||||
title: _(msg`Report this message`),
|
||||
subtitle: _(msg`Why should this message be reviewed?`),
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [_, subject])
|
||||
}
|
||||
@@ -1,679 +0,0 @@
|
||||
import React from 'react'
|
||||
import {Pressable, View} from 'react-native'
|
||||
import {ScrollView} from 'react-native-gesture-handler'
|
||||
import {AppBskyLabelerDefs} from '@atproto/api'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {wait} from '#/lib/async/wait'
|
||||
import {getLabelingServiceTitle} from '#/lib/moderation'
|
||||
import {sanitizeHandle} from '#/lib/strings/handles'
|
||||
import {Logger} from '#/logger'
|
||||
import {isNative} from '#/platform/detection'
|
||||
import {useMyLabelersQuery} from '#/state/queries/preferences'
|
||||
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
|
||||
import {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||
import {atoms as a, useGutters, useTheme} from '#/alf'
|
||||
import * as Admonition from '#/components/Admonition'
|
||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
import {useDelayedLoading} from '#/components/hooks/useDelayedLoading'
|
||||
import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Retry} from '#/components/icons/ArrowRotateCounterClockwise'
|
||||
import {
|
||||
Check_Stroke2_Corner0_Rounded as CheckThin,
|
||||
CheckThick_Stroke2_Corner0_Rounded as Check,
|
||||
} from '#/components/icons/Check'
|
||||
import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane'
|
||||
import {SquareArrowTopRight_Stroke2_Corner0_Rounded as SquareArrowTopRight} from '#/components/icons/SquareArrowTopRight'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times'
|
||||
import {createStaticClick, InlineLinkText, Link} from '#/components/Link'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {useSubmitReportMutation} from './action'
|
||||
import {DMCA_LINK} from './const'
|
||||
import {useCopyForSubject} from './copy'
|
||||
import {initialState, reducer} from './state'
|
||||
import {ReportDialogProps, ReportSubject} from './types'
|
||||
import {parseReportSubject} from './utils/parseReportSubject'
|
||||
import {ReportOption, useReportOptions} from './utils/useReportOptions'
|
||||
|
||||
export {useDialogControl as useReportDialogControl} from '#/components/Dialog'
|
||||
|
||||
const logger = Logger.create(Logger.Context.ReportDialog)
|
||||
|
||||
export function ReportDialog(
|
||||
props: Omit<ReportDialogProps, 'subject'> & {
|
||||
subject: ReportSubject
|
||||
},
|
||||
) {
|
||||
const subject = React.useMemo(
|
||||
() => parseReportSubject(props.subject),
|
||||
[props.subject],
|
||||
)
|
||||
const onClose = React.useCallback(() => {
|
||||
logger.metric('reportDialog:close', {})
|
||||
}, [])
|
||||
return (
|
||||
<Dialog.Outer control={props.control} onClose={onClose}>
|
||||
<Dialog.Handle />
|
||||
{subject ? <Inner {...props} subject={subject} /> : <Invalid />}
|
||||
</Dialog.Outer>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This should only be shown if the dialog is configured incorrectly by a
|
||||
* developer, but nevertheless we should have a graceful fallback.
|
||||
*/
|
||||
function Invalid() {
|
||||
const {_} = useLingui()
|
||||
return (
|
||||
<Dialog.ScrollableInner label={_(msg`Report dialog`)}>
|
||||
<Text style={[a.font_heavy, a.text_xl, a.leading_snug, a.pb_xs]}>
|
||||
<Trans>Invalid report subject</Trans>
|
||||
</Text>
|
||||
<Text style={[a.text_md, a.leading_snug]}>
|
||||
<Trans>
|
||||
Something wasn't quite right with the data you're trying to report.
|
||||
Please contact support.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
function Inner(props: ReportDialogProps) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const ref = React.useRef<ScrollView>(null)
|
||||
const {
|
||||
data: allLabelers,
|
||||
isLoading: isLabelerLoading,
|
||||
error: labelersLoadError,
|
||||
refetch: refetchLabelers,
|
||||
} = useMyLabelersQuery({excludeNonConfigurableLabelers: true})
|
||||
const isLoading = useDelayedLoading(500, isLabelerLoading)
|
||||
const copy = useCopyForSubject(props.subject)
|
||||
const reportOptions = useReportOptions()
|
||||
const [state, dispatch] = React.useReducer(reducer, initialState)
|
||||
|
||||
/**
|
||||
* Submission handling
|
||||
*/
|
||||
const {mutateAsync: submitReport} = useSubmitReportMutation()
|
||||
const [isPending, setPending] = React.useState(false)
|
||||
const [isSuccess, setSuccess] = React.useState(false)
|
||||
|
||||
/**
|
||||
* Labelers that support this `subject` and its NSID collection
|
||||
*/
|
||||
const supportedLabelers = React.useMemo(() => {
|
||||
if (!allLabelers) return []
|
||||
return allLabelers
|
||||
.filter(l => {
|
||||
const subjectTypes: string[] | undefined = l.subjectTypes
|
||||
if (subjectTypes === undefined) return true
|
||||
if (props.subject.type === 'account') {
|
||||
return subjectTypes.includes('account')
|
||||
} else if (props.subject.type === 'chatMessage') {
|
||||
return subjectTypes.includes('chat')
|
||||
} else {
|
||||
return subjectTypes.includes('record')
|
||||
}
|
||||
})
|
||||
.filter(l => {
|
||||
const collections: string[] | undefined = l.subjectCollections
|
||||
if (collections === undefined) return true
|
||||
// all chat collections accepted, since only Bluesky handles chats
|
||||
if (props.subject.type === 'chatMessage') return true
|
||||
return collections.includes(props.subject.nsid)
|
||||
})
|
||||
.filter(l => {
|
||||
if (!state.selectedOption) return true
|
||||
const reasonTypes: string[] | undefined = l.reasonTypes
|
||||
if (reasonTypes === undefined) return true
|
||||
return reasonTypes.includes(state.selectedOption.reason)
|
||||
})
|
||||
}, [props, allLabelers, state.selectedOption])
|
||||
const hasSupportedLabelers = !!supportedLabelers.length
|
||||
const hasSingleSupportedLabeler = supportedLabelers.length === 1
|
||||
|
||||
const onSubmit = React.useCallback(async () => {
|
||||
dispatch({type: 'clearError'})
|
||||
|
||||
logger.info('submitting')
|
||||
|
||||
try {
|
||||
setPending(true)
|
||||
// wait at least 1s, make it feel substantial
|
||||
await wait(
|
||||
1e3,
|
||||
submitReport({
|
||||
subject: props.subject,
|
||||
state,
|
||||
}),
|
||||
)
|
||||
setSuccess(true)
|
||||
logger.metric('reportDialog:success', {
|
||||
reason: state.selectedOption?.reason!,
|
||||
labeler: state.selectedLabeler?.creator.handle!,
|
||||
details: !!state.details,
|
||||
})
|
||||
// give time for user feedback
|
||||
setTimeout(() => {
|
||||
props.control.close()
|
||||
}, 1e3)
|
||||
} catch (e: any) {
|
||||
logger.metric('reportDialog:failure', {})
|
||||
logger.error(e, {
|
||||
source: 'ReportDialog',
|
||||
})
|
||||
dispatch({
|
||||
type: 'setError',
|
||||
error: _(msg`Something went wrong. Please try again.`),
|
||||
})
|
||||
} finally {
|
||||
setPending(false)
|
||||
}
|
||||
}, [_, submitReport, state, dispatch, props, setPending, setSuccess])
|
||||
|
||||
React.useEffect(() => {
|
||||
logger.metric('reportDialog:open', {
|
||||
subjectType: props.subject.type,
|
||||
})
|
||||
}, [props.subject])
|
||||
|
||||
return (
|
||||
<Dialog.ScrollableInner
|
||||
testID="report:dialog"
|
||||
label={_(msg`Report dialog`)}
|
||||
ref={ref}
|
||||
style={[a.w_full, {maxWidth: 500}]}>
|
||||
<View style={[a.gap_2xl, isNative && a.pt_md]}>
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={1}
|
||||
title={copy.subtitle}
|
||||
activeIndex1={state.activeStepIndex1}
|
||||
/>
|
||||
{isLoading ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
<OptionCardSkeleton />
|
||||
<OptionCardSkeleton />
|
||||
<OptionCardSkeleton />
|
||||
<OptionCardSkeleton />
|
||||
<OptionCardSkeleton />
|
||||
{/* Here to capture focus for a hot sec to prevent flash */}
|
||||
<Pressable accessible={false} />
|
||||
</View>
|
||||
) : labelersLoadError || !allLabelers ? (
|
||||
<Admonition.Outer type="error">
|
||||
<Admonition.Row>
|
||||
<Admonition.Icon />
|
||||
<Admonition.Text>
|
||||
<Trans>Something went wrong, please try again</Trans>
|
||||
</Admonition.Text>
|
||||
<Admonition.Button
|
||||
label={_(msg`Retry loading report options`)}
|
||||
onPress={() => refetchLabelers()}>
|
||||
<ButtonText>
|
||||
<Trans>Retry</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon icon={Retry} />
|
||||
</Admonition.Button>
|
||||
</Admonition.Row>
|
||||
</Admonition.Outer>
|
||||
) : (
|
||||
<>
|
||||
{state.selectedOption ? (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<OptionCard option={state.selectedOption} />
|
||||
</View>
|
||||
<Button
|
||||
testID="report:clearOption"
|
||||
label={_(msg`Change report reason`)}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => {
|
||||
dispatch({type: 'clearOption'})
|
||||
}}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</View>
|
||||
) : (
|
||||
<View style={[a.gap_sm]}>
|
||||
{reportOptions[props.subject.type].map(o => (
|
||||
<OptionCard
|
||||
key={o.reason}
|
||||
option={o}
|
||||
onSelect={() => {
|
||||
dispatch({type: 'selectOption', option: o})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
{['post', 'account'].includes(props.subject.type) && (
|
||||
<Link
|
||||
to={DMCA_LINK}
|
||||
label={_(
|
||||
msg`View details for reporting a copyright violation`,
|
||||
)}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.w_full,
|
||||
a.px_md,
|
||||
a.py_sm,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
hovered || pressed
|
||||
? [t.atoms.border_contrast_high]
|
||||
: [t.atoms.border_contrast_low],
|
||||
]}>
|
||||
<Text style={[a.flex_1, a.italic, a.leading_snug]}>
|
||||
<Trans>Need to report a copyright violation?</Trans>
|
||||
</Text>
|
||||
<SquareArrowTopRight
|
||||
size="sm"
|
||||
fill={t.atoms.text.color}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StepOuter>
|
||||
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={2}
|
||||
title={_(msg`Select moderation service`)}
|
||||
activeIndex1={state.activeStepIndex1}
|
||||
/>
|
||||
{state.activeStepIndex1 >= 2 && (
|
||||
<>
|
||||
{state.selectedLabeler ? (
|
||||
<>
|
||||
{hasSingleSupportedLabeler ? (
|
||||
<LabelerCard labeler={state.selectedLabeler} />
|
||||
) : (
|
||||
<View style={[a.flex_row, a.align_center, a.gap_md]}>
|
||||
<View style={[a.flex_1]}>
|
||||
<LabelerCard labeler={state.selectedLabeler} />
|
||||
</View>
|
||||
<Button
|
||||
label={_(msg`Change moderation service`)}
|
||||
size="tiny"
|
||||
variant="solid"
|
||||
color="secondary"
|
||||
shape="round"
|
||||
onPress={() => {
|
||||
dispatch({type: 'clearLabeler'})
|
||||
}}>
|
||||
<ButtonIcon icon={X} />
|
||||
</Button>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{hasSupportedLabelers ? (
|
||||
<View style={[a.gap_sm]}>
|
||||
{hasSingleSupportedLabeler ? (
|
||||
<>
|
||||
<LabelerCard labeler={supportedLabelers[0]} />
|
||||
<ActionOnce
|
||||
check={() => !state.selectedLabeler}
|
||||
callback={() => {
|
||||
dispatch({
|
||||
type: 'selectLabeler',
|
||||
labeler: supportedLabelers[0],
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{supportedLabelers.map(l => (
|
||||
<LabelerCard
|
||||
key={l.creator.did}
|
||||
labeler={l}
|
||||
onSelect={() => {
|
||||
dispatch({type: 'selectLabeler', labeler: l})
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
// should never happen in our app
|
||||
<Admonition.Admonition type="warning">
|
||||
<Trans>
|
||||
Unfortunately, none of your subscribed labelers supports
|
||||
this report type.
|
||||
</Trans>
|
||||
</Admonition.Admonition>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StepOuter>
|
||||
|
||||
<StepOuter>
|
||||
<StepTitle
|
||||
index={3}
|
||||
title={_(msg`Submit report`)}
|
||||
activeIndex1={state.activeStepIndex1}
|
||||
/>
|
||||
{state.activeStepIndex1 === 3 && (
|
||||
<>
|
||||
<View style={[a.pb_xs, a.gap_xs]}>
|
||||
<Text style={[a.leading_snug, a.pb_xs]}>
|
||||
<Trans>
|
||||
Your report will be sent to{' '}
|
||||
<Text style={[a.font_bold, a.leading_snug]}>
|
||||
{state.selectedLabeler?.creator.displayName}
|
||||
</Text>
|
||||
.
|
||||
</Trans>{' '}
|
||||
{!state.detailsOpen ? (
|
||||
<InlineLinkText
|
||||
label={_(msg`Add more details (optional)`)}
|
||||
{...createStaticClick(() => {
|
||||
dispatch({type: 'showDetails'})
|
||||
})}>
|
||||
<Trans>Add more details (optional)</Trans>
|
||||
</InlineLinkText>
|
||||
) : null}
|
||||
</Text>
|
||||
|
||||
{state.detailsOpen && (
|
||||
<View>
|
||||
<Dialog.Input
|
||||
testID="report:details"
|
||||
multiline
|
||||
value={state.details}
|
||||
onChangeText={details => {
|
||||
dispatch({type: 'setDetails', details})
|
||||
}}
|
||||
label={_(msg`Additional details (limit 300 characters)`)}
|
||||
style={{paddingRight: 60}}
|
||||
numberOfLines={4}
|
||||
/>
|
||||
<View
|
||||
style={[
|
||||
a.absolute,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.pr_md,
|
||||
a.pb_sm,
|
||||
{
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
},
|
||||
]}>
|
||||
<CharProgress count={state.details?.length || 0} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
<Button
|
||||
testID="report:submit"
|
||||
label={_(msg`Submit report`)}
|
||||
size="large"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
disabled={isPending || isSuccess}
|
||||
onPress={onSubmit}>
|
||||
<ButtonText>
|
||||
<Trans>Submit report</Trans>
|
||||
</ButtonText>
|
||||
<ButtonIcon
|
||||
icon={isSuccess ? CheckThin : isPending ? Loader : PaperPlane}
|
||||
/>
|
||||
</Button>
|
||||
|
||||
{state.error && (
|
||||
<Admonition.Admonition type="error">
|
||||
{state.error}
|
||||
</Admonition.Admonition>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</StepOuter>
|
||||
</View>
|
||||
|
||||
<Dialog.Close />
|
||||
</Dialog.ScrollableInner>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionOnce({
|
||||
check,
|
||||
callback,
|
||||
}: {
|
||||
check: () => boolean
|
||||
callback: () => void
|
||||
}) {
|
||||
React.useEffect(() => {
|
||||
if (check()) {
|
||||
callback()
|
||||
}
|
||||
}, [check, callback])
|
||||
return null
|
||||
}
|
||||
|
||||
function StepOuter({children}: {children: React.ReactNode}) {
|
||||
return <View style={[a.gap_md, a.w_full]}>{children}</View>
|
||||
}
|
||||
|
||||
function StepTitle({
|
||||
index,
|
||||
title,
|
||||
activeIndex1,
|
||||
}: {
|
||||
index: number
|
||||
title: string
|
||||
activeIndex1: number
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const active = activeIndex1 === index
|
||||
const completed = activeIndex1 > index
|
||||
return (
|
||||
<View style={[a.flex_row, a.gap_sm, a.pr_3xl]}>
|
||||
<View
|
||||
style={[
|
||||
a.justify_center,
|
||||
a.align_center,
|
||||
a.rounded_full,
|
||||
a.border,
|
||||
{
|
||||
width: 24,
|
||||
height: 24,
|
||||
backgroundColor: active
|
||||
? t.palette.primary_500
|
||||
: completed
|
||||
? t.palette.primary_100
|
||||
: t.atoms.bg_contrast_25.backgroundColor,
|
||||
borderColor: active
|
||||
? t.palette.primary_500
|
||||
: completed
|
||||
? t.palette.primary_400
|
||||
: t.atoms.border_contrast_low.borderColor,
|
||||
},
|
||||
]}>
|
||||
{completed ? (
|
||||
<Check width={12} />
|
||||
) : (
|
||||
<Text
|
||||
style={[
|
||||
a.font_heavy,
|
||||
a.text_center,
|
||||
t.atoms.text,
|
||||
{
|
||||
color: active
|
||||
? 'white'
|
||||
: completed
|
||||
? t.palette.primary_700
|
||||
: t.atoms.text_contrast_medium.color,
|
||||
fontVariant: ['tabular-nums'],
|
||||
width: 24,
|
||||
height: 24,
|
||||
lineHeight: 24,
|
||||
},
|
||||
]}>
|
||||
{index}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={[
|
||||
a.flex_1,
|
||||
a.font_heavy,
|
||||
a.text_lg,
|
||||
a.leading_snug,
|
||||
active ? t.atoms.text : t.atoms.text_contrast_medium,
|
||||
{
|
||||
top: 1,
|
||||
},
|
||||
]}>
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function OptionCard({
|
||||
option,
|
||||
onSelect,
|
||||
}: {
|
||||
option: ReportOption
|
||||
onSelect?: (option: ReportOption) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const gutters = useGutters(['compact'])
|
||||
const onPress = React.useCallback(() => {
|
||||
onSelect?.(option)
|
||||
}, [onSelect, option])
|
||||
return (
|
||||
<Button
|
||||
testID={`report:option:${option.reason}`}
|
||||
label={_(msg`Create report for ${option.title}`)}
|
||||
onPress={onPress}
|
||||
disabled={!onSelect}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
gutters,
|
||||
a.py_sm,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_25,
|
||||
hovered || pressed
|
||||
? [t.atoms.border_contrast_high]
|
||||
: [t.atoms.border_contrast_low],
|
||||
]}>
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||
{option.title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[a.text_sm, , a.leading_snug, t.atoms.text_contrast_medium]}>
|
||||
{option.description}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
function OptionCardSkeleton() {
|
||||
const t = useTheme()
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.rounded_sm,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_25,
|
||||
t.atoms.border_contrast_low,
|
||||
{height: 55}, // magic, based on web
|
||||
]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function LabelerCard({
|
||||
labeler,
|
||||
onSelect,
|
||||
}: {
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
onSelect?: (option: AppBskyLabelerDefs.LabelerViewDetailed) => void
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const {_} = useLingui()
|
||||
const onPress = React.useCallback(() => {
|
||||
onSelect?.(labeler)
|
||||
}, [onSelect, labeler])
|
||||
const title = getLabelingServiceTitle({
|
||||
displayName: labeler.creator.displayName,
|
||||
handle: labeler.creator.handle,
|
||||
})
|
||||
return (
|
||||
<Button
|
||||
testID={`report:labeler:${labeler.creator.handle}`}
|
||||
label={_(msg`Send report to ${title}`)}
|
||||
onPress={onPress}
|
||||
disabled={!onSelect}>
|
||||
{({hovered, pressed}) => (
|
||||
<View
|
||||
style={[
|
||||
a.w_full,
|
||||
a.p_sm,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_sm,
|
||||
a.rounded_md,
|
||||
a.border,
|
||||
t.atoms.bg_contrast_25,
|
||||
hovered || pressed
|
||||
? [t.atoms.border_contrast_high]
|
||||
: [t.atoms.border_contrast_low],
|
||||
]}>
|
||||
<UserAvatar
|
||||
type="labeler"
|
||||
size={36}
|
||||
avatar={labeler.creator.avatar}
|
||||
/>
|
||||
<View style={[a.flex_1]}>
|
||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||
{title}
|
||||
</Text>
|
||||
<Text
|
||||
style={[
|
||||
a.text_sm,
|
||||
,
|
||||
a.leading_snug,
|
||||
t.atoms.text_contrast_medium,
|
||||
]}>
|
||||
<Trans>By {sanitizeHandle(labeler.creator.handle, '@')}</Trans>
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import {AppBskyLabelerDefs, ComAtprotoModerationDefs} from '@atproto/api'
|
||||
|
||||
import {ReportOption} from './utils/useReportOptions'
|
||||
|
||||
export type ReportState = {
|
||||
selectedOption?: ReportOption
|
||||
selectedLabeler?: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
details?: string
|
||||
detailsOpen: boolean
|
||||
activeStepIndex1: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ReportAction =
|
||||
| {
|
||||
type: 'selectOption'
|
||||
option: ReportOption
|
||||
}
|
||||
| {
|
||||
type: 'clearOption'
|
||||
}
|
||||
| {
|
||||
type: 'selectLabeler'
|
||||
labeler: AppBskyLabelerDefs.LabelerViewDetailed
|
||||
}
|
||||
| {
|
||||
type: 'clearLabeler'
|
||||
}
|
||||
| {
|
||||
type: 'setDetails'
|
||||
details: string
|
||||
}
|
||||
| {
|
||||
type: 'setError'
|
||||
error: string
|
||||
}
|
||||
| {
|
||||
type: 'clearError'
|
||||
}
|
||||
| {
|
||||
type: 'showDetails'
|
||||
}
|
||||
|
||||
export const initialState: ReportState = {
|
||||
selectedOption: undefined,
|
||||
selectedLabeler: undefined,
|
||||
details: undefined,
|
||||
detailsOpen: false,
|
||||
activeStepIndex1: 1,
|
||||
}
|
||||
|
||||
export function reducer(state: ReportState, action: ReportAction): ReportState {
|
||||
switch (action.type) {
|
||||
case 'selectOption':
|
||||
return {
|
||||
...state,
|
||||
selectedOption: action.option,
|
||||
activeStepIndex1: 2,
|
||||
detailsOpen:
|
||||
!!state.details ||
|
||||
action.option.reason === ComAtprotoModerationDefs.REASONOTHER,
|
||||
}
|
||||
case 'clearOption':
|
||||
return {
|
||||
...state,
|
||||
selectedOption: undefined,
|
||||
selectedLabeler: undefined,
|
||||
activeStepIndex1: 1,
|
||||
detailsOpen:
|
||||
!!state.details ||
|
||||
state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER,
|
||||
}
|
||||
case 'selectLabeler':
|
||||
return {
|
||||
...state,
|
||||
selectedLabeler: action.labeler,
|
||||
activeStepIndex1: 3,
|
||||
}
|
||||
case 'clearLabeler':
|
||||
return {
|
||||
...state,
|
||||
selectedLabeler: undefined,
|
||||
activeStepIndex1: 2,
|
||||
detailsOpen:
|
||||
!!state.details ||
|
||||
state.selectedOption?.reason === ComAtprotoModerationDefs.REASONOTHER,
|
||||
}
|
||||
case 'setDetails':
|
||||
return {
|
||||
...state,
|
||||
details: action.details,
|
||||
}
|
||||
case 'setError':
|
||||
return {
|
||||
...state,
|
||||
error: action.error,
|
||||
}
|
||||
case 'clearError':
|
||||
return {
|
||||
...state,
|
||||
error: undefined,
|
||||
}
|
||||
case 'showDetails':
|
||||
return {
|
||||
...state,
|
||||
detailsOpen: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import {
|
||||
$Typed,
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyGraphDefs,
|
||||
ChatBskyConvoDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
import * as Dialog from '#/components/Dialog'
|
||||
|
||||
export type ReportSubject =
|
||||
| $Typed<AppBskyActorDefs.ProfileViewBasic>
|
||||
| $Typed<AppBskyActorDefs.ProfileView>
|
||||
| $Typed<AppBskyActorDefs.ProfileViewDetailed>
|
||||
| $Typed<AppBskyGraphDefs.ListView>
|
||||
| $Typed<AppBskyFeedDefs.GeneratorView>
|
||||
| $Typed<AppBskyGraphDefs.StarterPackView>
|
||||
| $Typed<AppBskyFeedDefs.PostView>
|
||||
| {convoId: string; message: ChatBskyConvoDefs.MessageView}
|
||||
|
||||
export type ParsedReportSubject =
|
||||
| {
|
||||
type: 'post'
|
||||
uri: string
|
||||
cid: string
|
||||
nsid: string
|
||||
attributes: {
|
||||
reply: boolean
|
||||
image: boolean
|
||||
video: boolean
|
||||
link: boolean
|
||||
quote: boolean
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'list'
|
||||
uri: string
|
||||
cid: string
|
||||
nsid: string
|
||||
}
|
||||
| {
|
||||
type: 'feed'
|
||||
uri: string
|
||||
cid: string
|
||||
nsid: string
|
||||
}
|
||||
| {
|
||||
type: 'starterPack'
|
||||
uri: string
|
||||
cid: string
|
||||
nsid: string
|
||||
}
|
||||
| {
|
||||
type: 'account'
|
||||
did: string
|
||||
nsid: string
|
||||
}
|
||||
| {
|
||||
type: 'chatMessage'
|
||||
convoId: string
|
||||
message: ChatBskyConvoDefs.MessageView
|
||||
}
|
||||
|
||||
export type ReportDialogProps = {
|
||||
control: Dialog.DialogOuterProps['control']
|
||||
subject: ParsedReportSubject
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
import {
|
||||
AppBskyActorDefs,
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
AppBskyGraphDefs,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {
|
||||
ParsedReportSubject,
|
||||
ReportSubject,
|
||||
} from '#/components/moderation/ReportDialog/types'
|
||||
import * as bsky from '#/types/bsky'
|
||||
|
||||
export function parseReportSubject(
|
||||
subject: ReportSubject,
|
||||
): ParsedReportSubject | undefined {
|
||||
if (!subject) return
|
||||
|
||||
if ('convoId' in subject) {
|
||||
return {
|
||||
type: 'chatMessage',
|
||||
...subject,
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
AppBskyActorDefs.isProfileViewBasic(subject) ||
|
||||
AppBskyActorDefs.isProfileView(subject) ||
|
||||
AppBskyActorDefs.isProfileViewDetailed(subject)
|
||||
) {
|
||||
return {
|
||||
type: 'account',
|
||||
did: subject.did,
|
||||
nsid: 'app.bsky.actor.profile',
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isListView(subject)) {
|
||||
return {
|
||||
type: 'list',
|
||||
uri: subject.uri,
|
||||
cid: subject.cid,
|
||||
nsid: 'app.bsky.graph.list',
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isGeneratorView(subject)) {
|
||||
return {
|
||||
type: 'feed',
|
||||
uri: subject.uri,
|
||||
cid: subject.cid,
|
||||
nsid: 'app.bsky.feed.generator',
|
||||
}
|
||||
} else if (AppBskyGraphDefs.isStarterPackView(subject)) {
|
||||
return {
|
||||
type: 'starterPack',
|
||||
uri: subject.uri,
|
||||
cid: subject.cid,
|
||||
nsid: 'app.bsky.graph.starterPack',
|
||||
}
|
||||
} else if (AppBskyFeedDefs.isPostView(subject)) {
|
||||
const record = subject.record
|
||||
const embed = bsky.post.parseEmbed(subject.embed)
|
||||
if (
|
||||
bsky.dangerousIsType<AppBskyFeedPost.Record>(
|
||||
record,
|
||||
AppBskyFeedPost.isRecord,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: 'post',
|
||||
uri: subject.uri,
|
||||
cid: subject.cid,
|
||||
nsid: 'app.bsky.feed.post',
|
||||
attributes: {
|
||||
reply: !!record.reply,
|
||||
image:
|
||||
embed.type === 'images' ||
|
||||
(embed.type === 'post_with_media' && embed.media.type === 'images'),
|
||||
video:
|
||||
embed.type === 'video' ||
|
||||
(embed.type === 'post_with_media' && embed.media.type === 'video'),
|
||||
link:
|
||||
embed.type === 'link' ||
|
||||
(embed.type === 'post_with_media' && embed.media.type === 'link'),
|
||||
quote:
|
||||
embed.type === 'post' ||
|
||||
(embed.type === 'post_with_media' &&
|
||||
(embed.view.type === 'post' ||
|
||||
embed.view.type === 'post_with_media')),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import {useMemo} from 'react'
|
||||
import {ComAtprotoModerationDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
export interface ReportOption {
|
||||
reason: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
interface ReportOptions {
|
||||
account: ReportOption[]
|
||||
post: ReportOption[]
|
||||
list: ReportOption[]
|
||||
starterPack: ReportOption[]
|
||||
feed: ReportOption[]
|
||||
chatMessage: ReportOption[]
|
||||
}
|
||||
|
||||
export function useReportOptions(): ReportOptions {
|
||||
const {_} = useLingui()
|
||||
|
||||
return useMemo(() => {
|
||||
const other = {
|
||||
reason: ComAtprotoModerationDefs.REASONOTHER,
|
||||
title: _(msg`Other`),
|
||||
description: _(msg`An issue not included in these options`),
|
||||
}
|
||||
const common = [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONRUDE,
|
||||
title: _(msg`Anti-Social Behavior`),
|
||||
description: _(msg`Harassment, trolling, or intolerance`),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||
title: _(msg`Illegal and Urgent`),
|
||||
description: _(msg`Glaring violations of law or terms of service`),
|
||||
},
|
||||
other,
|
||||
]
|
||||
return {
|
||||
account: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONMISLEADING,
|
||||
title: _(msg`Misleading Account`),
|
||||
description: _(
|
||||
msg`Impersonation or false claims about identity or affiliation`,
|
||||
),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONSPAM,
|
||||
title: _(msg`Frequently Posts Unwanted Content`),
|
||||
description: _(msg`Spam; excessive mentions or replies`),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||
title: _(msg`Name or Description Violates Community Standards`),
|
||||
description: _(msg`Terms used violate community standards`),
|
||||
},
|
||||
other,
|
||||
],
|
||||
post: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONMISLEADING,
|
||||
title: _(msg`Misleading Post`),
|
||||
description: _(msg`Impersonation, misinformation, or false claims`),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONSPAM,
|
||||
title: _(msg`Spam`),
|
||||
description: _(msg`Excessive mentions or replies`),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONSEXUAL,
|
||||
title: _(msg`Unwanted Sexual Content`),
|
||||
description: _(msg`Nudity or adult content not labeled as such`),
|
||||
},
|
||||
...common,
|
||||
],
|
||||
chatMessage: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONSPAM,
|
||||
title: _(msg`Spam`),
|
||||
description: _(msg`Excessive or unwanted messages`),
|
||||
},
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONSEXUAL,
|
||||
title: _(msg`Unwanted Sexual Content`),
|
||||
description: _(msg`Inappropriate messages or explicit links`),
|
||||
},
|
||||
...common,
|
||||
],
|
||||
list: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||
title: _(msg`Name or Description Violates Community Standards`),
|
||||
description: _(msg`Terms used violate community standards`),
|
||||
},
|
||||
...common,
|
||||
],
|
||||
starterPack: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||
title: _(msg`Name or Description Violates Community Standards`),
|
||||
description: _(msg`Terms used violate community standards`),
|
||||
},
|
||||
...common,
|
||||
],
|
||||
feed: [
|
||||
{
|
||||
reason: ComAtprotoModerationDefs.REASONVIOLATION,
|
||||
title: _(msg`Name or Description Violates Community Standards`),
|
||||
description: _(msg`Terms used violate community standards`),
|
||||
},
|
||||
...common,
|
||||
],
|
||||
}
|
||||
}, [_])
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export async function wait<T>(delay: number, fn: T): Promise<Awaited<T>> {
|
||||
return await Promise.all([fn, new Promise(y => setTimeout(y, delay))]).then(
|
||||
arr => arr[0],
|
||||
)
|
||||
}
|
||||
@@ -168,9 +168,6 @@ export const MAX_LABELERS = 20
|
||||
export const VIDEO_SERVICE = 'https://video.bsky.app'
|
||||
export const VIDEO_SERVICE_DID = 'did:web:video.bsky.app'
|
||||
|
||||
export const VIDEO_MAX_DURATION_MS = 3 * 60 * 1000 // 3 minutes in milliseconds
|
||||
export const VIDEO_MAX_SIZE = 1000 * 1000 * 100 // 100mb
|
||||
|
||||
export const SUPPORTED_MIME_TYPES = [
|
||||
'video/mp4',
|
||||
'video/mpeg',
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {ImagePickerAsset} from 'expo-image-picker'
|
||||
|
||||
import {VIDEO_MAX_SIZE} from '#/lib/constants'
|
||||
import {VideoTooLargeError} from '#/lib/media/video/errors'
|
||||
import {CompressedVideo} from './types'
|
||||
|
||||
const MAX_VIDEO_SIZE = 1000 * 1000 * 50 // 50mb
|
||||
|
||||
// doesn't actually compress, converts to ArrayBuffer
|
||||
export async function compressVideo(
|
||||
asset: ImagePickerAsset,
|
||||
@@ -16,7 +17,7 @@ export async function compressVideo(
|
||||
const blob = base64ToBlob(base64, mimeType)
|
||||
const uri = URL.createObjectURL(blob)
|
||||
|
||||
if (blob.size > VIDEO_MAX_SIZE) {
|
||||
if (blob.size > MAX_VIDEO_SIZE) {
|
||||
throw new VideoTooLargeError()
|
||||
}
|
||||
|
||||
|
||||
@@ -56,9 +56,8 @@ export type CommonNavigatorParams = {
|
||||
Search: {q?: string}
|
||||
Hashtag: {tag: string; author?: string}
|
||||
Topic: {topic: string}
|
||||
MessagesConversation: {conversation: string; embed?: string; accept?: true}
|
||||
MessagesConversation: {conversation: string; embed?: string}
|
||||
MessagesSettings: undefined
|
||||
MessagesInbox: undefined
|
||||
NotificationSettings: undefined
|
||||
Feeds: undefined
|
||||
Start: {name: string; rkey: string}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type MetricEvents = {
|
||||
export type LogEvents = {
|
||||
// App events
|
||||
init: {
|
||||
initMs: number
|
||||
@@ -30,7 +30,7 @@ export type MetricEvents = {
|
||||
secondsActive: number
|
||||
}
|
||||
'state:foreground': {}
|
||||
'router:navigate': {
|
||||
'lake:router:navigate': {
|
||||
from?: string
|
||||
}
|
||||
'deepLink:referrerReceived': {
|
||||
@@ -305,28 +305,4 @@ export type MetricEvents = {
|
||||
|
||||
'progressGuide:hide': {}
|
||||
'progressGuide:followDialog:open': {}
|
||||
|
||||
'moderation:subscribedToLabeler': {}
|
||||
'moderation:unsubscribedFromLabeler': {}
|
||||
'moderation:changeLabelPreference': {
|
||||
preference: string
|
||||
}
|
||||
|
||||
'moderation:subscribedToList': {
|
||||
listType: 'mute' | 'block'
|
||||
}
|
||||
'moderation:unsubscribedFromList': {
|
||||
listType: 'mute' | 'block'
|
||||
}
|
||||
|
||||
'reportDialog:open': {
|
||||
subjectType: string
|
||||
}
|
||||
'reportDialog:close': {}
|
||||
'reportDialog:success': {
|
||||
reason: string
|
||||
labeler: string
|
||||
details: boolean
|
||||
}
|
||||
'reportDialog:failure': {}
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
|
||||
|
||||
import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
|
||||
import {logger} from '#/logger'
|
||||
import {MetricEvents} from '#/logger/metrics'
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import * as persisted from '#/state/persisted'
|
||||
import {useSession} from '../../state/session'
|
||||
import {timeout} from '../async/timeout'
|
||||
import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
|
||||
import {LogEvents} from './events'
|
||||
import {Gate} from './gates'
|
||||
|
||||
const SDK_KEY = 'client-SXJakO39w9vIhl3D44u8UupyzFl4oZ2qPIkjwcvuPsV'
|
||||
@@ -42,7 +42,7 @@ if (isWeb && typeof window !== 'undefined') {
|
||||
refUrl = decodeURIComponent(params.get('ref_url') ?? '')
|
||||
}
|
||||
|
||||
export type {MetricEvents as LogEvents}
|
||||
export type {LogEvents}
|
||||
|
||||
function createStatsigOptions(prefetchUsers: StatsigUser[]) {
|
||||
return {
|
||||
@@ -91,44 +91,25 @@ export function toClout(n: number | null | undefined): number | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `logger.metric()` instead
|
||||
*/
|
||||
export function logEvent<E extends keyof MetricEvents>(
|
||||
export function logEvent<E extends keyof LogEvents>(
|
||||
eventName: E & string,
|
||||
rawMetadata: MetricEvents[E] & FlatJSONRecord,
|
||||
options: {
|
||||
/**
|
||||
* Send to our data lake only, not to StatSig
|
||||
*/
|
||||
lake?: boolean
|
||||
} = {lake: false},
|
||||
rawMetadata: LogEvents[E] & FlatJSONRecord,
|
||||
) {
|
||||
try {
|
||||
const fullMetadata = toStringRecord(rawMetadata)
|
||||
fullMetadata.routeName = getCurrentRouteName() ?? '(Uninitialized)'
|
||||
if (Statsig.initializeCalled()) {
|
||||
let ev: string = eventName
|
||||
if (options.lake) {
|
||||
ev = `lake:${ev}`
|
||||
}
|
||||
Statsig.logEvent(ev, null, fullMetadata)
|
||||
}
|
||||
/**
|
||||
* All datalake events should be sent using `logger.metric`, and we don't
|
||||
* want to double-emit logs to other transports.
|
||||
*/
|
||||
if (!options.lake) {
|
||||
logger.info(eventName, fullMetadata)
|
||||
Statsig.logEvent(eventName, null, fullMetadata)
|
||||
}
|
||||
logger.info(eventName, fullMetadata)
|
||||
} catch (e) {
|
||||
// A log should never interrupt the calling code, whatever happens.
|
||||
logger.error('Failed to log an event', {message: e})
|
||||
}
|
||||
}
|
||||
|
||||
function toStringRecord<E extends keyof MetricEvents>(
|
||||
metadata: MetricEvents[E] & FlatJSONRecord,
|
||||
function toStringRecord<E extends keyof LogEvents>(
|
||||
metadata: LogEvents[E] & FlatJSONRecord,
|
||||
): Record<string, string> {
|
||||
const record: Record<string, string> = {}
|
||||
for (let key in metadata) {
|
||||
|
||||
@@ -323,7 +323,7 @@ export function createBskyAppAbsoluteUrl(path: string): string {
|
||||
export function createProxiedUrl(url: string): string {
|
||||
let u
|
||||
try {
|
||||
u = URL.parse(url)
|
||||
u = new URL(url)
|
||||
} catch {
|
||||
return url
|
||||
}
|
||||
|
||||
@@ -161,8 +161,6 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.ast
|
||||
case 'ca':
|
||||
return AppLanguage.ca
|
||||
case 'cy':
|
||||
return AppLanguage.cy
|
||||
case 'da':
|
||||
return AppLanguage.da
|
||||
case 'de':
|
||||
@@ -171,8 +169,6 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.el
|
||||
case 'en-GB':
|
||||
return AppLanguage.en_GB
|
||||
case 'eo':
|
||||
return AppLanguage.eo
|
||||
case 'es':
|
||||
return AppLanguage.es
|
||||
case 'eu':
|
||||
@@ -183,8 +179,6 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
|
||||
return AppLanguage.fr
|
||||
case 'ga':
|
||||
return AppLanguage.ga
|
||||
case 'gd':
|
||||
return AppLanguage.gd
|
||||
case 'gl':
|
||||
return AppLanguage.gl
|
||||
case 'hi':
|
||||
|
||||
@@ -14,19 +14,16 @@ import {AppLanguage} from '#/locale/languages'
|
||||
import {messages as messagesAn} from '#/locale/locales/an/messages'
|
||||
import {messages as messagesAst} from '#/locale/locales/ast/messages'
|
||||
import {messages as messagesCa} from '#/locale/locales/ca/messages'
|
||||
import {messages as messagesCy} from '#/locale/locales/cy/messages'
|
||||
import {messages as messagesDa} from '#/locale/locales/da/messages'
|
||||
import {messages as messagesDe} from '#/locale/locales/de/messages'
|
||||
import {messages as messagesEl} from '#/locale/locales/el/messages'
|
||||
import {messages as messagesEn} from '#/locale/locales/en/messages'
|
||||
import {messages as messagesEn_GB} from '#/locale/locales/en-GB/messages'
|
||||
import {messages as messagesEo} from '#/locale/locales/eo/messages'
|
||||
import {messages as messagesEs} from '#/locale/locales/es/messages'
|
||||
import {messages as messagesEu} from '#/locale/locales/eu/messages'
|
||||
import {messages as messagesFi} from '#/locale/locales/fi/messages'
|
||||
import {messages as messagesFr} from '#/locale/locales/fr/messages'
|
||||
import {messages as messagesGa} from '#/locale/locales/ga/messages'
|
||||
import {messages as messagesGd} from '#/locale/locales/gd/messages'
|
||||
import {messages as messagesGl} from '#/locale/locales/gl/messages'
|
||||
import {messages as messagesHi} from '#/locale/locales/hi/messages'
|
||||
import {messages as messagesHu} from '#/locale/locales/hu/messages'
|
||||
@@ -81,14 +78,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.cy: {
|
||||
i18n.loadAndActivate({locale, messages: messagesCy})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/cy'),
|
||||
import('@formatjs/intl-numberformat/locale-data/cy'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
i18n.loadAndActivate({locale, messages: messagesDa})
|
||||
await Promise.all([
|
||||
@@ -121,14 +110,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.eo: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEo})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/eo'),
|
||||
import('@formatjs/intl-numberformat/locale-data/eo'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
i18n.loadAndActivate({locale, messages: messagesEs})
|
||||
await Promise.all([
|
||||
@@ -169,14 +150,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.gd: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGd})
|
||||
await Promise.all([
|
||||
import('@formatjs/intl-pluralrules/locale-data/gd'),
|
||||
import('@formatjs/intl-numberformat/locale-data/gd'),
|
||||
])
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
i18n.loadAndActivate({locale, messages: messagesGl})
|
||||
await Promise.all([
|
||||
|
||||
@@ -24,10 +24,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ca/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.cy: {
|
||||
mod = await import(`./locales/cy/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.da: {
|
||||
mod = await import(`./locales/da/messages`)
|
||||
break
|
||||
@@ -44,10 +40,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/en-GB/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.eo: {
|
||||
mod = await import(`./locales/eo/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.es: {
|
||||
mod = await import(`./locales/es/messages`)
|
||||
break
|
||||
@@ -68,10 +60,6 @@ export async function dynamicActivate(locale: AppLanguage) {
|
||||
mod = await import(`./locales/ga/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gd: {
|
||||
mod = await import(`./locales/gd/messages`)
|
||||
break
|
||||
}
|
||||
case AppLanguage.gl: {
|
||||
mod = await import(`./locales/gl/messages`)
|
||||
break
|
||||
|
||||
+5
-11
@@ -9,18 +9,15 @@ export enum AppLanguage {
|
||||
an = 'an',
|
||||
ast = 'ast',
|
||||
ca = 'ca',
|
||||
cy = 'cy',
|
||||
da = 'da',
|
||||
de = 'de',
|
||||
el = 'el',
|
||||
en_GB = 'en-GB',
|
||||
eo = 'eo',
|
||||
es = 'es',
|
||||
eu = 'eu',
|
||||
fi = 'fi',
|
||||
fr = 'fr',
|
||||
ga = 'ga',
|
||||
gd = 'gd',
|
||||
gl = 'gl',
|
||||
hi = 'hi',
|
||||
hu = 'hu',
|
||||
@@ -56,18 +53,15 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
|
||||
{code2: AppLanguage.an, name: 'Aragonés – Aragonese'},
|
||||
{code2: AppLanguage.ast, name: 'Asturianu – Asturian'},
|
||||
{code2: AppLanguage.ca, name: 'Català – Catalan'},
|
||||
{code2: AppLanguage.cy, name: 'Cymraeg – Welsh'},
|
||||
{code2: AppLanguage.da, name: 'Dansk – Danish'},
|
||||
{code2: AppLanguage.de, name: 'Deutsch – German'},
|
||||
{code2: AppLanguage.el, name: 'Ελληνικά – Greek'},
|
||||
{code2: AppLanguage.en_GB, name: 'English (UK)'},
|
||||
{code2: AppLanguage.eo, name: 'Esperanto'},
|
||||
{code2: AppLanguage.es, name: 'Español – Spanish'},
|
||||
{code2: AppLanguage.eu, name: 'Euskera – Basque'},
|
||||
{code2: AppLanguage.fi, name: 'Suomi – Finnish'},
|
||||
{code2: AppLanguage.fr, name: 'Français – French'},
|
||||
{code2: AppLanguage.ga, name: 'Gaeilge – Irish'},
|
||||
{code2: AppLanguage.gd, name: 'Gàidhlig – Scottish Gaelic'},
|
||||
{code2: AppLanguage.gl, name: 'Galego – Galician'},
|
||||
{code2: AppLanguage.hi, name: 'हिंदी – Hindi'},
|
||||
{code2: AppLanguage.hu, name: 'magyar – Hungarian'},
|
||||
@@ -169,7 +163,7 @@ export const LANGUAGES: Language[] = [
|
||||
{code3: 'cad', code2: '', name: 'Caddo'},
|
||||
{code3: 'cai', code2: '', name: 'Central American Indian languages'},
|
||||
{code3: 'car', code2: '', name: 'Galibi Carib'},
|
||||
{code3: 'cat', code2: 'ca', name: 'Catalan'},
|
||||
{code3: 'cat', code2: 'ca', name: 'Catalan; Valencian'},
|
||||
{code3: 'cau', code2: '', name: 'Caucasian languages'},
|
||||
{code3: 'ceb', code2: '', name: 'Cebuano'},
|
||||
{code3: 'cel', code2: '', name: 'Celtic languages'},
|
||||
@@ -224,7 +218,7 @@ export const LANGUAGES: Language[] = [
|
||||
{code3: 'efi', code2: '', name: 'Efik'},
|
||||
{code3: 'egy', code2: '', name: 'Egyptian (Ancient)'},
|
||||
{code3: 'eka', code2: '', name: 'Ekajuk'},
|
||||
{code3: 'ell', code2: 'el', name: 'Greek'},
|
||||
{code3: 'ell', code2: 'el', name: 'Greek, Modern (1453-)'},
|
||||
{code3: 'elx', code2: '', name: 'Elamite'},
|
||||
{code3: 'eng', code2: 'en', name: 'English'},
|
||||
{code3: 'enm', code2: '', name: 'English, Middle (1100-1500)'},
|
||||
@@ -269,8 +263,8 @@ export const LANGUAGES: Language[] = [
|
||||
{code3: 'gor', code2: '', name: 'Gorontalo'},
|
||||
{code3: 'got', code2: '', name: 'Gothic'},
|
||||
{code3: 'grb', code2: '', name: 'Grebo'},
|
||||
{code3: 'grc', code2: '', name: 'Ancient Greek'},
|
||||
{code3: 'gre', code2: 'el', name: 'Greek'},
|
||||
{code3: 'grc', code2: '', name: 'Greek, Ancient (to 1453)'},
|
||||
{code3: 'gre', code2: 'el', name: 'Greek, Modern (1453-)'},
|
||||
{code3: 'grn', code2: 'gn', name: 'Guarani'},
|
||||
{code3: 'gsw', code2: '', name: 'Swiss German; Alemannic; Alsatian'},
|
||||
{code3: 'guj', code2: 'gu', name: 'Gujarati'},
|
||||
@@ -367,7 +361,7 @@ export const LANGUAGES: Language[] = [
|
||||
{code3: 'lit', code2: 'lt', name: 'Lithuanian'},
|
||||
{code3: 'lol', code2: '', name: 'Mongo'},
|
||||
{code3: 'loz', code2: '', name: 'Lozi'},
|
||||
{code3: 'ltz', code2: 'lb', name: 'Luxembourgish'},
|
||||
{code3: 'ltz', code2: 'lb', name: 'Luxembourgish; Letzeburgesch'},
|
||||
{code3: 'lua', code2: '', name: 'Luba-Lulua'},
|
||||
{code3: 'lub', code2: 'lu', name: 'Luba-Katanga'},
|
||||
{code3: 'lug', code2: 'lg', name: 'Ganda'},
|
||||
|
||||
+1206
-1626
File diff suppressed because it is too large
Load Diff
+1140
-1560
File diff suppressed because it is too large
Load Diff
+1160
-1580
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1208
-1628
File diff suppressed because it is too large
Load Diff
+1142
-1562
File diff suppressed because it is too large
Load Diff
+1145
-1565
File diff suppressed because it is too large
Load Diff
+1145
-1565
File diff suppressed because it is too large
Load Diff
+583
-985
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1307
-1727
File diff suppressed because it is too large
Load Diff
+1164
-1584
File diff suppressed because it is too large
Load Diff
+1155
-1575
File diff suppressed because it is too large
Load Diff
+1137
-1557
File diff suppressed because it is too large
Load Diff
+1156
-1576
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1348
-1768
File diff suppressed because it is too large
Load Diff
+1151
-1571
File diff suppressed because it is too large
Load Diff
+1135
-1555
File diff suppressed because it is too large
Load Diff
+1167
-1587
File diff suppressed because it is too large
Load Diff
+1529
-1949
File diff suppressed because it is too large
Load Diff
+1150
-1570
File diff suppressed because it is too large
Load Diff
+1212
-1632
File diff suppressed because it is too large
Load Diff
+1139
-1559
File diff suppressed because it is too large
Load Diff
+1143
-1563
File diff suppressed because it is too large
Load Diff
+1144
-1564
File diff suppressed because it is too large
Load Diff
+1222
-1642
File diff suppressed because it is too large
Load Diff
+1205
-1625
File diff suppressed because it is too large
Load Diff
+1342
-1762
File diff suppressed because it is too large
Load Diff
+1276
-1696
File diff suppressed because it is too large
Load Diff
+1309
-1729
File diff suppressed because it is too large
Load Diff
+1148
-1568
File diff suppressed because it is too large
Load Diff
+1142
-1562
File diff suppressed because it is too large
Load Diff
+1180
-1600
File diff suppressed because it is too large
Load Diff
+1141
-1561
File diff suppressed because it is too large
Load Diff
+1166
-1586
File diff suppressed because it is too large
Load Diff
+1169
-1589
File diff suppressed because it is too large
Load Diff
+1230
-1650
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user