Merge remote-tracking branch 'origin/main' into eric/nova

* origin/main: (67 commits)
  Localize options in "Thread Preferences" screen (#2373)
  Bump ios build number and android version code
  Remove the KeyboardAvoidingView in account creation (close #2333) (#2366)
  1.62
  Create account tweaks (#2365)
  Fix sizing of the leftnav new post btn (#2248)
  Use memory caching for android lightbox (#2354)
  Disable page-transition animations on android (#2352)
  Disable BlurView on android (#2351)
  Handle birth dates as UTC, handle locale formatting (#2363)
  Fix desktop styles a bit
  Disable spanish translation until it's more thoroughly reviewed (#2362)
  minor search screen ux improvements (#2264)
  Temporarily disable the german translation (#2360)
  Web dropdowns (#2358)
  Mark more text as translatable (#2284)
  Remove patched color scheme code (#2340)
  support multiple og:image tags (#2305)
  Fix missing avatar moderation in replies (#2325)
  Fixes to feed load triggers (#2323)
  ...
This commit is contained in:
Eric Bailey
2024-01-01 16:38:38 -06:00
130 changed files with 11913 additions and 2484 deletions
+140 -27
View File
@@ -1,6 +1,5 @@
import {RichText} from '@atproto/api' import {RichText} from '@atproto/api'
import { import {
getYoutubeVideoId,
makeRecordUri, makeRecordUri,
toNiceDomain, toNiceDomain,
toShortUrl, toShortUrl,
@@ -12,6 +11,7 @@ import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip' import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles' import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
import {cleanError} from '../../src/lib/strings/errors' import {cleanError} from '../../src/lib/strings/errors'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
describe('detectLinkables', () => { describe('detectLinkables', () => {
const inputs = [ const inputs = [
@@ -335,32 +335,6 @@ describe('toShareUrl', () => {
}) })
}) })
describe('getYoutubeVideoId', () => {
it(' should return undefined for invalid youtube links', () => {
expect(getYoutubeVideoId('')).toBeUndefined()
expect(getYoutubeVideoId('https://www.google.com')).toBeUndefined()
expect(getYoutubeVideoId('https://www.youtube.com')).toBeUndefined()
expect(
getYoutubeVideoId('https://www.youtube.com/channelName'),
).toBeUndefined()
expect(
getYoutubeVideoId('https://www.youtube.com/channel/channelName'),
).toBeUndefined()
})
it('getYoutubeVideoId should return video id for valid youtube links', () => {
expect(getYoutubeVideoId('https://www.youtube.com/watch?v=videoId')).toBe(
'videoId',
)
expect(
getYoutubeVideoId(
'https://www.youtube.com/watch?v=videoId&feature=share',
),
).toBe('videoId')
expect(getYoutubeVideoId('https://youtu.be/videoId')).toBe('videoId')
})
})
describe('shortenLinks', () => { describe('shortenLinks', () => {
const inputs = [ const inputs = [
'start https://middle.com/foo/bar?baz=bux#hash end', 'start https://middle.com/foo/bar?baz=bux#hash end',
@@ -396,6 +370,7 @@ describe('shortenLinks', () => {
], ],
], ],
] ]
it('correctly shortens rich text while preserving facet URIs', () => { it('correctly shortens rich text while preserving facet URIs', () => {
for (let i = 0; i < inputs.length; i++) { for (let i = 0; i < inputs.length; i++) {
const input = inputs[i] const input = inputs[i]
@@ -410,3 +385,141 @@ describe('shortenLinks', () => {
} }
}) })
}) })
describe('parseEmbedPlayerFromUrl', () => {
const inputs = [
'https://youtu.be/videoId',
'https://www.youtube.com/watch?v=videoId',
'https://www.youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/watch?v=videoId',
'https://youtube.com/watch?v=videoId&feature=share',
'https://youtube.com/shorts/videoId',
'https://youtube.com/shorts/',
'https://youtube.com/',
'https://youtube.com/random',
'https://twitch.tv/channelName',
'https://www.twitch.tv/channelName',
'https://open.spotify.com/playlist/playlistId',
'https://open.spotify.com/playlist/playlistId?param=value',
'https://open.spotify.com/track/songId',
'https://open.spotify.com/track/songId?param=value',
'https://open.spotify.com/album/albumId',
'https://open.spotify.com/album/albumId?param=value',
'https://soundcloud.com/user/track',
'https://soundcloud.com/user/sets/set',
'https://soundcloud.com/user/',
]
const outputs = [
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
{
type: 'youtube_video',
videoId: 'videoId',
playerUri: 'https://www.youtube.com/embed/videoId?autoplay=1',
},
undefined,
undefined,
undefined,
{
type: 'twitch_live',
channelId: 'channelName',
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
},
{
type: 'twitch_live',
channelId: 'channelName',
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=channelName&parent=localhost`,
},
{
type: 'spotify_playlist',
playlistId: 'playlistId',
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
},
{
type: 'spotify_playlist',
playlistId: 'playlistId',
playerUri: `https://open.spotify.com/embed/playlist/playlistId`,
},
{
type: 'spotify_song',
songId: 'songId',
playerUri: `https://open.spotify.com/embed/track/songId`,
},
{
type: 'spotify_song',
songId: 'songId',
playerUri: `https://open.spotify.com/embed/track/songId`,
},
{
type: 'spotify_album',
albumId: 'albumId',
playerUri: `https://open.spotify.com/embed/album/albumId`,
},
{
type: 'spotify_album',
albumId: 'albumId',
playerUri: `https://open.spotify.com/embed/album/albumId`,
},
{
type: 'soundcloud_track',
user: 'user',
track: 'track',
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/track&auto_play=true&visual=false&hide_related=true`,
},
{
type: 'soundcloud_set',
user: 'user',
set: 'set',
playerUri: `https://w.soundcloud.com/player/?url=https://soundcloud.com/user/sets/set&auto_play=true&visual=false&hide_related=true`,
},
undefined,
]
it('correctly grabs the correct id from uri', () => {
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i]
const output = outputs[i]
const res = parseEmbedPlayerFromUrl(input)
console.log(input)
expect(res).toEqual(output)
}
})
})
+5 -2
View File
@@ -9,12 +9,12 @@ module.exports = function () {
/** /**
* iOS build number. Must be incremented for each TestFlight version. * iOS build number. Must be incremented for each TestFlight version.
*/ */
const IOS_BUILD_NUMBER = '4' const IOS_BUILD_NUMBER = '2'
/** /**
* Android build number. Must be incremented for each release. * Android build number. Must be incremented for each release.
*/ */
const ANDROID_VERSION_CODE = 50 const ANDROID_VERSION_CODE = 53
/** /**
* Uses built-in Expo env vars * Uses built-in Expo env vars
@@ -110,6 +110,9 @@ module.exports = function () {
[ [
'expo-build-properties', 'expo-build-properties',
{ {
ios: {
deploymentTarget: '13.4',
},
android: { android: {
compileSdkVersion: 34, compileSdkVersion: 34,
targetSdkVersion: 34, targetSdkVersion: 34,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 223 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

After

Width:  |  Height:  |  Size: 452 KiB

+1
View File
@@ -42,6 +42,7 @@ module.exports = function (api) {
platform: './src/platform', platform: './src/platform',
state: './src/state', state: './src/state',
view: './src/view', view: './src/view',
crypto: './src/platform/crypto.ts',
}, },
}, },
], ],
+47 -10
View File
@@ -1,8 +1,11 @@
package main package main
import ( import (
"encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"strings"
"time"
appbsky "github.com/bluesky-social/indigo/api/bsky" appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/atproto/syntax"
@@ -10,6 +13,12 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ItemGUID struct {
XMLName xml.Name `xml:"guid"`
Value string `xml:",chardata"`
IsPerma bool `xml:"isPermaLink,attr"`
}
// We don't actually populate the title for "posts". // We don't actually populate the title for "posts".
// Some background: https://book.micro.blog/rss-for-microblogs/ // Some background: https://book.micro.blog/rss-for-microblogs/
type Item struct { type Item struct {
@@ -17,8 +26,7 @@ type Item struct {
Link string `xml:"link,omitempty"` Link string `xml:"link,omitempty"`
Description string `xml:"description,omitempty"` Description string `xml:"description,omitempty"`
PubDate string `xml:"pubDate,omitempty"` PubDate string `xml:"pubDate,omitempty"`
Author string `xml:"author,omitempty"` GUID ItemGUID
GUID string `xml:"guid,omitempty"`
} }
type rss struct { type rss struct {
@@ -32,11 +40,33 @@ type rss struct {
func (srv *Server) WebProfileRSS(c echo.Context) error { func (srv *Server) WebProfileRSS(c echo.Context) error {
ctx := c.Request().Context() ctx := c.Request().Context()
req := c.Request()
didParam := c.Param("did") identParam := c.Param("ident")
did, err := syntax.ParseDID(didParam)
// if not a DID, try parsing as a handle and doing a redirect
if !strings.HasPrefix(identParam, "did:") {
handle, err := syntax.ParseHandle(identParam)
if err != nil { if err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", didParam)) return echo.NewHTTPError(400, fmt.Sprintf("not a valid handle: %s", identParam))
}
// check that public view is Ok, and resolve DID
pv, err := appbsky.ActorGetProfile(ctx, srv.xrpcc, handle.String())
if err != nil {
return echo.NewHTTPError(404, fmt.Sprintf("account not found: %s", handle))
}
for _, label := range pv.Labels {
if label.Src == pv.Did && label.Val == "!no-unauthenticated" {
return echo.NewHTTPError(403, fmt.Sprintf("account does not allow public views: %s", handle))
}
}
return c.Redirect(http.StatusFound, fmt.Sprintf("/profile/%s/rss", pv.Did))
}
did, err := syntax.ParseDID(identParam)
if err != nil {
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", identParam))
} }
// check that public view is Ok // check that public view is Ok
@@ -71,12 +101,19 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
if rec.Reply != nil { if rec.Reply != nil {
continue continue
} }
pubDate := ""
createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt)
if nil == err {
pubDate = createdAt.Time().Format(time.RFC822Z)
}
posts = append(posts, Item{ posts = append(posts, Item{
Link: fmt.Sprintf("https://bsky.app/profile/%s/post/%s", pv.Handle, aturi.RecordKey().String()), Link: fmt.Sprintf("https://%s/profile/%s/post/%s", req.Host, pv.Handle, aturi.RecordKey().String()),
Description: rec.Text, Description: rec.Text,
PubDate: rec.CreatedAt, PubDate: pubDate,
Author: "@" + pv.Handle, GUID: ItemGUID{
GUID: aturi.String(), Value: aturi.String(),
IsPerma: false,
},
}) })
} }
@@ -91,7 +128,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
feed := &rss{ feed := &rss{
Version: "2.0", Version: "2.0",
Description: desc, Description: desc,
Link: fmt.Sprintf("https://bsky.app/profile/%s", pv.Handle), Link: fmt.Sprintf("https://%s/profile/%s", req.Host, pv.Handle),
Title: title, Title: title,
Item: posts, Item: posts,
} }
+7 -2
View File
@@ -210,7 +210,7 @@ func serve(cctx *cli.Context) error {
e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric) e.GET("/profile/:handle/feed/:rkey/liked-by", server.WebGeneric)
// profile RSS feed (DID not handle) // profile RSS feed (DID not handle)
e.GET("/profile/:did/rss", server.WebProfileRSS) e.GET("/profile/:ident/rss", server.WebProfileRSS)
// post endpoints; only first populates info // post endpoints; only first populates info
e.GET("/profile/:handle/post/:rkey", server.WebPost) e.GET("/profile/:handle/post/:rkey", server.WebPost)
@@ -336,7 +336,11 @@ func (srv *Server) WebPost(c echo.Context) error {
data["postView"] = postView data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
if postView.Embed != nil && postView.Embed.EmbedImages_View != nil { if postView.Embed != nil && postView.Embed.EmbedImages_View != nil {
data["imgThumbUrl"] = postView.Embed.EmbedImages_View.Images[0].Thumb var thumbUrls []string
for i := range postView.Embed.EmbedImages_View.Images {
thumbUrls = append(thumbUrls, postView.Embed.EmbedImages_View.Images[i].Thumb)
}
data["imgThumbUrls"] = thumbUrls
} }
return c.Render(http.StatusOK, "post.html", data) return c.Render(http.StatusOK, "post.html", data)
} }
@@ -370,6 +374,7 @@ func (srv *Server) WebProfile(c echo.Context) error {
req := c.Request() req := c.Request()
data["profileView"] = pv data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path) data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
data["requestHost"] = req.Host
return c.Render(http.StatusOK, "profile.html", data) return c.Render(http.StatusOK, "profile.html", data)
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 769 B

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+3 -1
View File
@@ -25,8 +25,10 @@
<meta name="description" content="{{ postView.Record.Val.Text }}"> <meta name="description" content="{{ postView.Record.Val.Text }}">
<meta property="og:description" content="{{ postView.Record.Val.Text }}"> <meta property="og:description" content="{{ postView.Record.Val.Text }}">
{% endif -%} {% endif -%}
{%- if imgThumbUrl %} {%- if imgThumbUrls %}
{% for imgThumbUrl in imgThumbUrls %}
<meta property="og:image" content="{{ imgThumbUrl }}"> <meta property="og:image" content="{{ imgThumbUrl }}">
{% endfor %}
<meta name="twitter:card" content="summary_large_image"> <meta name="twitter:card" content="summary_large_image">
{%- elif postView.Author.Avatar %} {%- elif postView.Author.Avatar %}
{# Don't use avatar image in cards; usually looks bad #} {# Don't use avatar image in cards; usually looks bad #}
+3 -1
View File
@@ -34,7 +34,9 @@
{% endif %} {% endif %}
<meta name="twitter:label1" content="Account DID"> <meta name="twitter:label1" content="Account DID">
<meta name="twitter:value1" content="{{ profileView.Did }}"> <meta name="twitter:value1" content="{{ profileView.Did }}">
<link rel="alternate" type="application/rss+xml" href="/profile/{{ profileView.Did }}/rss"> {%- if requestHost %}
<link rel="alternate" type="application/rss+xml" href="https://{{ requestHost }}/profile/{{ profileView.Did }}/rss">
{% endif %}
{% endif -%} {% endif -%}
{%- endblock %} {%- endblock %}
+1 -1
View File
@@ -32,7 +32,7 @@ import { Text } from "react-native";
const text = "Hello World"; const text = "Hello World";
<Text accessibilityLabel="Label is here">{text}</Text> <Text accessibilityLabel="Label is here">{text}</Text>
``` ```
In this case, you cannot use the `useLingui()` hook: In this case, you can use the `useLingui()` hook:
```jsx ```jsx
import { msg } from "@lingui/macro"; import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react"; import { useLingui } from "@lingui/react";
+1 -1
View File
@@ -1,6 +1,6 @@
/** @type {import('@lingui/conf').LinguiConfig} */ /** @type {import('@lingui/conf').LinguiConfig} */
module.exports = { module.exports = {
locales: ['en', 'hi', 'ja'], locales: ['en', 'hi', 'ja', 'fr', 'de', 'es'],
catalogs: [ catalogs: [
{ {
path: '<rootDir>/src/locale/locales/{locale}/messages', path: '<rootDir>/src/locale/locales/{locale}/messages',
+50 -38
View File
@@ -1,10 +1,13 @@
{ {
"name": "bsky.app", "name": "bsky.app",
"version": "1.60.0", "version": "1.62.0",
"private": true, "private": true,
"engines": {
"node": ">=18"
},
"scripts": { "scripts": {
"prepare": "is-ci || husky install", "prepare": "is-ci || husky install",
"postinstall": "patch-package", "postinstall": "patch-package && yarn intl:compile",
"prebuild": "expo prebuild --clean", "prebuild": "expo prebuild --clean",
"android": "expo run:android", "android": "expo run:android",
"ios": "expo run:ios", "ios": "expo run:ios",
@@ -32,7 +35,8 @@
"intl:build": "yarn intl:check && yarn intl:compile", "intl:build": "yarn intl:check && yarn intl:compile",
"intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)", "intl:check": "yarn intl:extract && git diff-index -G'(^[^\\*# /])|(^#\\w)|(^\\s+[^\\*#/])' HEAD || (echo '\n⚠️ i18n detected un-extracted translations\n' && exit 1)",
"intl:extract": "lingui extract", "intl:extract": "lingui extract",
"intl:compile": "lingui compile" "intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
}, },
"dependencies": { "dependencies": {
"@atproto/api": "^0.7.4", "@atproto/api": "^0.7.4",
@@ -49,14 +53,14 @@
"@lingui/react": "^4.5.0", "@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4", "@mattermost/react-native-paste-input": "^0.6.4",
"@miblanchard/react-native-slider": "^2.3.1", "@miblanchard/react-native-slider": "^2.3.1",
"@react-native-async-storage/async-storage": "1.18.2", "@react-native-async-storage/async-storage": "1.21.0",
"@react-native-camera-roll/camera-roll": "^5.2.2", "@react-native-camera-roll/camera-roll": "^5.2.2",
"@react-native-clipboard/clipboard": "^1.10.0", "@react-native-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0", "@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.2.0", "@react-native-community/datetimepicker": "7.6.1",
"@react-native-masked-view/masked-view": "^0.3.1", "@react-native-masked-view/masked-view": "0.3.0",
"@react-native-menu/menu": "^0.8.0", "@react-native-menu/menu": "^0.8.0",
"@react-native-picker/picker": "2.4.10", "@react-native-picker/picker": "2.5.1",
"@react-navigation/bottom-tabs": "^6.5.7", "@react-navigation/bottom-tabs": "^6.5.7",
"@react-navigation/drawer": "^6.6.2", "@react-navigation/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6", "@react-navigation/native": "^6.1.6",
@@ -65,7 +69,7 @@
"@segment/analytics-react": "^1.0.0-rc1", "@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1", "@segment/analytics-react-native": "^2.10.1",
"@segment/sovran-react-native": "^0.4.5", "@segment/sovran-react-native": "^0.4.5",
"@sentry/react-native": "5.10.0", "@sentry/react-native": "5.5.0",
"@tanstack/react-query": "^5.8.1", "@tanstack/react-query": "^5.8.1",
"@tiptap/core": "^2.0.0-beta.220", "@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220", "@tiptap/extension-document": "^2.0.0-beta.220",
@@ -90,24 +94,25 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
"expo": "^49.0.8", "expo": "^50.0.0-preview.7",
"expo-application": "~5.3.0", "expo-application": "~5.8.1",
"expo-build-properties": "~0.8.3", "expo-build-properties": "^0.11.0",
"expo-camera": "13.5.1", "expo-camera": "~14.0.1",
"expo-constants": "~14.4.2", "expo-constants": "~15.4.2",
"expo-dev-client": "2.4.7", "expo-dev-client": "~3.3.4",
"expo-device": "~5.4.0", "expo-device": "~5.9.1",
"expo-image": "~1.3.2", "expo-image": "~1.10.1",
"expo-image-manipulator": "~11.5.0", "expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.5.0", "expo-image-picker": "~14.7.1",
"expo-localization": "~14.3.0", "expo-localization": "~14.8.1",
"expo-media-library": "~15.4.1", "expo-media-library": "~15.9.1",
"expo-notifications": "~0.20.1", "expo-notifications": "~0.27.2",
"expo-sharing": "~11.5.0", "expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.20.5", "expo-splash-screen": "~0.26.1",
"expo-status-bar": "~1.6.0", "expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.4.0", "expo-system-ui": "~2.9.2",
"expo-updates": "~0.18.12", "expo-task-manager": "~11.7.0",
"expo-updates": "~0.24.5",
"fast-text-encoding": "^1.0.6", "fast-text-encoding": "^1.0.6",
"history": "^5.3.0", "history": "^5.3.0",
"js-sha256": "^0.9.0", "js-sha256": "^0.9.0",
@@ -135,31 +140,34 @@
"react-avatar-editor": "^13.0.0", "react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.0", "react-circular-progressbar": "^2.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-native": "0.72.5", "react-native": "0.73.1",
"react-native-appstate-hook": "^1.0.6", "react-native-appstate-hook": "^1.0.6",
"react-native-drawer-layout": "^3.2.0", "react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0", "react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.12.1", "react-native-gesture-handler": "~2.14.0",
"react-native-get-random-values": "^1.8.0", "react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^1.14.0", "react-native-haptic-feedback": "^1.14.0",
"react-native-image-crop-picker": "^0.38.1", "react-native-image-crop-picker": "^0.38.1",
"react-native-inappbrowser-reborn": "^3.6.3", "react-native-inappbrowser-reborn": "^3.6.3",
"react-native-ios-context-menu": "^1.15.3", "react-native-ios-context-menu": "^1.15.3",
"react-native-linear-gradient": "^2.6.2", "react-native-linear-gradient": "^2.6.2",
"react-native-pager-view": "6.1.4", "react-native-pager-view": "6.2.2",
"react-native-picker-select": "^8.1.0", "react-native-picker-select": "^8.1.0",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.6.0", "react-native-reanimated": "^3.6.0",
"react-native-root-siblings": "^4.1.1", "react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.6.3", "react-native-safe-area-context": "4.7.4",
"react-native-screens": "~3.22.0", "react-native-screens": "~3.27.0",
"react-native-splash-screen": "^3.3.0", "react-native-splash-screen": "^3.3.0",
"react-native-svg": "13.9.0", "react-native-svg": "14.0.0",
"react-native-url-polyfill": "^1.3.0", "react-native-url-polyfill": "^1.3.0",
"react-native-uuid": "^2.0.1", "react-native-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6", "react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6", "react-native-web": "~0.19.6",
"react-native-web-linear-gradient": "^1.1.2", "react-native-web-linear-gradient": "^1.1.2",
"react-native-web-webview": "^1.0.2",
"react-native-webview": "^13.6.3",
"react-native-youtube-iframe": "^2.3.0",
"react-responsive": "^9.0.2", "react-responsive": "^9.0.2",
"rn-fetch-blob": "^0.12.0", "rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1", "sentry-expo": "~7.0.1",
@@ -175,10 +183,13 @@
"@babel/preset-env": "^7.20.0", "@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0", "@babel/runtime": "^7.20.0",
"@did-plc/server": "^0.0.1", "@did-plc/server": "^0.0.1",
"@expo/config-plugins": "7.8.0",
"@expo/prebuild-config": "6.7.0",
"@lingui/cli": "^4.5.0", "@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0", "@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0", "@react-native-community/eslint-config": "^3.0.0",
"@react-native/typescript-config": "^0.74.0",
"@testing-library/jest-native": "^5.4.1", "@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2", "@testing-library/react-native": "^11.5.2",
"@tsconfig/react-native": "^2.0.3", "@tsconfig/react-native": "^2.0.3",
@@ -199,11 +210,12 @@
"@types/react-test-renderer": "^17.0.1", "@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^5.48.2", "@typescript-eslint/eslint-plugin": "^5.48.2",
"@typescript-eslint/parser": "^5.48.2", "@typescript-eslint/parser": "^5.48.2",
"babel-jest": "^29.4.2", "babel-jest": "^29.7.0",
"babel-loader": "^9.1.2", "babel-loader": "^9.1.2",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0", "babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12", "babel-plugin-react-native-web": "^0.18.12",
"babel-preset-expo": "^10.0.0",
"detox": "^20.13.0", "detox": "^20.13.0",
"eslint": "^8.19.0", "eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0", "eslint-plugin-detox": "^1.0.0",
@@ -214,8 +226,8 @@
"html-webpack-plugin": "^5.5.0", "html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3", "husky": "^8.0.3",
"is-ci": "^3.0.1", "is-ci": "^3.0.1",
"jest": "^29.4.3", "jest": "^29.7.0",
"jest-expo": "^49.0.0", "jest-expo": "^50.0.1",
"jest-junit": "^15.0.0", "jest-junit": "^15.0.0",
"lint-staged": "^13.2.3", "lint-staged": "^13.2.3",
"metro-react-native-babel-preset": "^0.73.7", "metro-react-native-babel-preset": "^0.73.7",
@@ -225,7 +237,7 @@
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"typescript": "^5.1.3", "typescript": "^5.3.3",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"webpack": "^5.75.0", "webpack": "^5.75.0",
"webpack-cli": "^5.0.1", "webpack-cli": "^5.0.1",
-14
View File
@@ -1,14 +0,0 @@
diff --git a/node_modules/babel-preset-expo/index.js b/node_modules/babel-preset-expo/index.js
index 2099ee3..2b9e092 100644
--- a/node_modules/babel-preset-expo/index.js
+++ b/node_modules/babel-preset-expo/index.js
@@ -105,7 +105,8 @@ module.exports = function (api, options = {}) {
],
],
plugins: [
- getObjectRestSpreadPlugin(),
+ // - dan: This will be disabled anyway when we upgrade Expo, but let's do it now.
+ // getObjectRestSpreadPlugin(),
...extraPlugins,
getAliasPlugin(),
[require.resolve('@babel/plugin-proposal-decorators'), { legacy: true }],
@@ -1,8 +1,8 @@
diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js diff --git a/node_modules/metro-transform-worker/src/index.js b/node_modules/metro-transform-worker/src/index.js
index 27d4cb3..fd71f47 100644 index cae11e7..42f251b 100644
--- a/node_modules/metro-transform-worker/src/index.js --- a/node_modules/metro-transform-worker/src/index.js
+++ b/node_modules/metro-transform-worker/src/index.js +++ b/node_modules/metro-transform-worker/src/index.js
@@ -190,6 +190,10 @@ async function transformJS(file, { config, options, projectRoot }) { @@ -189,6 +189,10 @@ async function transformJS(file, { config, options, projectRoot }) {
let dependencyMapName = ""; let dependencyMapName = "";
let dependencies; let dependencies;
let wrappedAst; let wrappedAst;
@@ -13,7 +13,7 @@ index 27d4cb3..fd71f47 100644
// If the module to transform is a script (meaning that is not part of the // If the module to transform is a script (meaning that is not part of the
// dependency graph and it code will just be prepended to the bundle modules), // dependency graph and it code will just be prepended to the bundle modules),
@@ -229,19 +233,20 @@ async function transformJS(file, { config, options, projectRoot }) { @@ -228,19 +232,20 @@ async function transformJS(file, { config, options, projectRoot }) {
if (config.unstable_disableModuleWrapping === true) { if (config.unstable_disableModuleWrapping === true) {
wrappedAst = ast; wrappedAst = ast;
} else { } else {
@@ -1,7 +1,7 @@
diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m diff --git a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
index 9dca6a5..090bda5 100644 index 9dca6a5..090bda5 100644
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m --- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
+++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m +++ b/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.mm
@@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo @@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo
- (void)textViewDidChangeSelection:(__unused UITextView *)textView - (void)textViewDidChangeSelection:(__unused UITextView *)textView
@@ -1,54 +0,0 @@
diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index ab0fc7f..1ace752 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -1,6 +1,6 @@
#import "ReactNativePageView.h"
-#import "React/RCTLog.h"
+#import <React/RCTLog.h>
#import <React/RCTViewManager.h>
#import "UIViewController+CreateExtension.h"
@@ -9,7 +9,7 @@
#import "RCTOnPageSelected.h"
#import <math.h>
-@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate>
+@interface ReactNativePageView () <UIPageViewControllerDataSource, UIPageViewControllerDelegate, UIScrollViewDelegate, UIGestureRecognizerDelegate>
@property(nonatomic, strong) UIPageViewController *reactPageViewController;
@property(nonatomic, strong) RCTEventDispatcher *eventDispatcher;
@@ -80,6 +80,10 @@ - (void)didMoveToWindow {
[self setupInitialController];
}
+ UIPanGestureRecognizer* panGestureRecognizer = [UIPanGestureRecognizer new];
+ panGestureRecognizer.delegate = self;
+ [self addGestureRecognizer: panGestureRecognizer];
+
if (self.reactViewController.navigationController != nil && self.reactViewController.navigationController.interactivePopGestureRecognizer != nil) {
[self.scrollView.panGestureRecognizer requireGestureRecognizerToFail:self.reactViewController.navigationController.interactivePopGestureRecognizer];
}
@@ -463,4 +467,21 @@ - (NSString *)determineScrollDirection:(UIScrollView *)scrollView {
- (BOOL)isLtrLayout {
return [_layoutDirection isEqualToString:@"ltr"];
}
+
+- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
+ if (!_overdrag && otherGestureRecognizer == self.scrollView.panGestureRecognizer) {
+ UIPanGestureRecognizer* p = (UIPanGestureRecognizer*) gestureRecognizer;
+ CGPoint velocity = [p velocityInView:self];
+ if (self.currentIndex == 0 && velocity.x > 0) {
+ self.scrollView.panGestureRecognizer.enabled = false;
+ return NO;
+ } else {
+ self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+ }
+ } else {
+ self.scrollView.panGestureRecognizer.enabled = self.scrollEnabled;
+ }
+
+ return YES;
+}
@end
+6 -1
View File
@@ -26,7 +26,7 @@ import {BottomBar} from './view/shell/bottom-bar/BottomBar'
import {buildStateObject} from 'lib/routes/helpers' import {buildStateObject} from 'lib/routes/helpers'
import {State, RouteParams} from 'lib/routes/types' import {State, RouteParams} from 'lib/routes/types'
import {colors} from 'lib/styles' import {colors} from 'lib/styles'
import {isNative} from 'platform/detection' import {isAndroid, isNative} from 'platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes' import {router} from './routes'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
@@ -286,6 +286,7 @@ function HomeTabNavigator() {
return ( return (
<HomeTab.Navigator <HomeTab.Navigator
screenOptions={{ screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true, gestureEnabled: true,
fullScreenGestureEnabled: true, fullScreenGestureEnabled: true,
headerShown: false, headerShown: false,
@@ -307,6 +308,7 @@ function SearchTabNavigator() {
return ( return (
<SearchTab.Navigator <SearchTab.Navigator
screenOptions={{ screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true, gestureEnabled: true,
fullScreenGestureEnabled: true, fullScreenGestureEnabled: true,
headerShown: false, headerShown: false,
@@ -324,6 +326,7 @@ function FeedsTabNavigator() {
return ( return (
<FeedsTab.Navigator <FeedsTab.Navigator
screenOptions={{ screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true, gestureEnabled: true,
fullScreenGestureEnabled: true, fullScreenGestureEnabled: true,
headerShown: false, headerShown: false,
@@ -345,6 +348,7 @@ function NotificationsTabNavigator() {
return ( return (
<NotificationsTab.Navigator <NotificationsTab.Navigator
screenOptions={{ screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true, gestureEnabled: true,
fullScreenGestureEnabled: true, fullScreenGestureEnabled: true,
headerShown: false, headerShown: false,
@@ -366,6 +370,7 @@ function MyProfileTabNavigator() {
return ( return (
<MyProfileTab.Navigator <MyProfileTab.Navigator
screenOptions={{ screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true, gestureEnabled: true,
fullScreenGestureEnabled: true, fullScreenGestureEnabled: true,
headerShown: false, headerShown: false,
+20 -15
View File
@@ -40,8 +40,6 @@ type Props = {
isReady: boolean isReady: boolean
} }
SplashScreen.preventAutoHideAsync().catch(() => {})
const AnimatedLogo = Animated.createAnimatedComponent(Logo) const AnimatedLogo = Animated.createAnimatedComponent(Logo)
export function Splash(props: React.PropsWithChildren<Props>) { export function Splash(props: React.PropsWithChildren<Props>) {
@@ -49,6 +47,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
const intro = useSharedValue(0) const intro = useSharedValue(0)
const outroLogo = useSharedValue(0) const outroLogo = useSharedValue(0)
const outroApp = useSharedValue(0) const outroApp = useSharedValue(0)
const outroAppOpacity = useSharedValue(0)
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false) const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
const [isImageLoaded, setIsImageLoaded] = React.useState(false) const [isImageLoaded, setIsImageLoaded] = React.useState(false)
const isReady = props.isReady && isImageLoaded const isReady = props.isReady && isImageLoaded
@@ -62,8 +61,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
{ {
scale: interpolate( scale: interpolate(
outroLogo.value, outroLogo.value,
[0, 0.06, 0.08, 1], [0, 0.08, 1],
[1, 0.8, 0.8, 400], [1, 0.8, 400],
'clamp', 'clamp',
), ),
}, },
@@ -79,7 +78,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'), scale: interpolate(outroApp.value, [0, 1], [1.1, 1], 'clamp'),
}, },
], ],
opacity: interpolate(outroApp.value, [0, 0.9, 1], [0, 1, 1], 'clamp'), opacity: interpolate(
outroAppOpacity.value,
[0, 0.08, 0.15, 1],
[0, 0, 1, 1],
'clamp',
),
} }
}) })
@@ -92,29 +96,30 @@ export function Splash(props: React.PropsWithChildren<Props>) {
intro.value = withTiming( intro.value = withTiming(
1, 1,
{duration: 200, easing: Easing.out(Easing.cubic)}, {duration: 400, easing: Easing.out(Easing.cubic)},
async () => { async () => {
// set these values to check animation at specific point // set these values to check animation at specific point
// outroLogo.value = 0.1 // outroLogo.value = 0.1
// outroApp.value = 0.1 // outroApp.value = 0.1
outroLogo.value = withTiming( outroLogo.value = withTiming(
1, 1,
{duration: 1000, easing: Easing.in(Easing.cubic)}, {duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(
1,
{duration: 1000, easing: Easing.inOut(Easing.cubic)},
() => { () => {
runOnJS(onFinish)() runOnJS(onFinish)()
}, },
) )
outroApp.value = withTiming(1, {
duration: 1200,
easing: Easing.inOut(Easing.cubic),
})
outroAppOpacity.value = withTiming(1, {
duration: 1200,
easing: Easing.in(Easing.cubic),
})
}, },
) )
} }
}, [onFinish, intro, outroLogo, outroApp, isReady]) }, [onFinish, intro, outroLogo, outroApp, outroAppOpacity, isReady])
const onLoadEnd = useCallback(() => { const onLoadEnd = useCallback(() => {
setIsImageLoaded(true) setIsImageLoaded(true)
+3 -2
View File
@@ -1,9 +1,10 @@
import {useColorScheme} from 'react-native'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
import {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
export function useColorModeTheme( export function useColorModeTheme(
theme: persisted.Schema['colorMode'], theme: persisted.Schema['colorMode'],
): 'light' | 'dark' { ): 'light' | 'dark' {
const colorScheme = useColorScheme_FIXED() const colorScheme = useColorScheme()
return (theme === 'system' ? colorScheme : theme) || 'light' return (theme === 'system' ? colorScheme : theme) || 'light'
} }
+7 -3
View File
@@ -1,7 +1,11 @@
import React, {ReactNode, createContext, useContext} from 'react' import React, {ReactNode, createContext, useContext} from 'react'
import {TextStyle, ViewStyle, ColorSchemeName} from 'react-native' import {
TextStyle,
useColorScheme,
ViewStyle,
ColorSchemeName,
} from 'react-native'
import {darkTheme, defaultTheme} from './themes' import {darkTheme, defaultTheme} from './themes'
import {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
export type ColorScheme = 'light' | 'dark' export type ColorScheme = 'light' | 'dark'
@@ -95,7 +99,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
theme, theme,
children, children,
}) => { }) => {
const colorScheme = useColorScheme_FIXED() const colorScheme = useColorScheme()
const themeValue = getTheme(theme === 'system' ? colorScheme : theme) const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
return ( return (
-1
View File
@@ -13,7 +13,6 @@ interface TrackPropertiesMap {
'Sign In': {resumedSession: boolean} // CAN BE SERVER 'Sign In': {resumedSession: boolean} // CAN BE SERVER
'Create Account': {} // CAN BE SERVER 'Create Account': {} // CAN BE SERVER
'Try Create Account': {} 'Try Create Account': {}
'Create Account Successfully': {}
'Signin:PressedForgotPassword': {} 'Signin:PressedForgotPassword': {}
'Signin:PressedSelectService': {} 'Signin:PressedSelectService': {}
// COMPOSER / CREATE POST events // COMPOSER / CREATE POST events
+10 -9
View File
@@ -117,11 +117,7 @@ export class FeedViewPostsSlice {
} }
export class NoopFeedTuner { export class NoopFeedTuner {
private keyCounter = 0 reset() {}
reset() {
this.keyCounter = 0
}
tune( tune(
feed: FeedViewPost[], feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean}, _opts?: {dryRun: boolean; maintainOrder: boolean},
@@ -131,13 +127,13 @@ export class NoopFeedTuner {
} }
export class FeedTuner { export class FeedTuner {
private keyCounter = 0 seenKeys: Set<string> = new Set()
seenUris: Set<string> = new Set() seenUris: Set<string> = new Set()
constructor(public tunerFns: FeedTunerFn[]) {} constructor(public tunerFns: FeedTunerFn[]) {}
reset() { reset() {
this.keyCounter = 0 this.seenKeys.clear()
this.seenUris.clear() this.seenUris.clear()
} }
@@ -218,11 +214,16 @@ export class FeedTuner {
} }
if (!dryRun) { if (!dryRun) {
for (const slice of slices) { slices = slices.filter(slice => {
if (this.seenKeys.has(slice._reactKey)) {
return false
}
for (const item of slice.items) { for (const item of slice.items) {
this.seenUris.add(item.post.uri) this.seenUris.add(item.post.uri)
} }
} this.seenKeys.add(slice._reactKey)
return true
})
} }
return slices return slices
-34
View File
@@ -1,34 +0,0 @@
import React from 'react'
import {
AppState,
useColorScheme as useColorScheme_BUGGY,
ColorSchemeName,
} from 'react-native'
import {isWeb} from '#/platform/detection'
/**
* With RN iOS, we can only "trust" the color scheme reported while the app is
* active. This is a workaround until the bug is fixed upstream.
*
* @see https://github.com/bluesky-social/social-app/pull/1417#issuecomment-1719868504
* @see https://github.com/facebook/react-native/pull/39439
*/
export function useColorScheme_FIXED() {
const colorScheme = useColorScheme_BUGGY()
const [currentColorScheme, setCurrentColorScheme] =
React.useState<ColorSchemeName>(colorScheme)
React.useEffect(() => {
// we don't need to be updating state on web
if (isWeb) return
const subscription = AppState.addEventListener('change', state => {
const isActive = state === 'active'
if (!isActive) return
setCurrentColorScheme(colorScheme)
})
return () => subscription.remove()
}, [colorScheme])
return isWeb ? colorScheme : currentColorScheme
}
+16
View File
@@ -0,0 +1,16 @@
export function usePhotoLibraryPermission() {
const requestPhotoAccessIfNeeded = async () => {
// On the, we use <input type="file"> to produce a filepicker
// This does not need any permission granting.
return true
}
return {requestPhotoAccessIfNeeded}
}
export function useCameraPermission() {
const requestCameraAccessIfNeeded = async () => {
return false
}
return {requestCameraAccessIfNeeded}
}
+58
View File
@@ -0,0 +1,58 @@
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
moderatePost,
} from '@atproto/api'
type ModeratePost = typeof moderatePost
type Options = Parameters<ModeratePost>[1] & {
hiddenPosts?: string[]
}
export function moderatePost_wrapped(
subject: Parameters<ModeratePost>[0],
opts: Options,
) {
const {hiddenPosts = [], ...options} = opts
const moderations = moderatePost(subject, options)
if (hiddenPosts.includes(subject.uri)) {
moderations.content.filter = true
moderations.content.blur = true
if (!moderations.content.cause) {
moderations.content.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'post-hidden',
source: {type: 'user'},
priority: 1,
}
}
}
if (subject.embed) {
let embedHidden = false
if (AppBskyEmbedRecord.isViewRecord(subject.embed.record)) {
embedHidden = hiddenPosts.includes(subject.embed.record.uri)
}
if (
AppBskyEmbedRecordWithMedia.isView(subject.embed) &&
AppBskyEmbedRecord.isViewRecord(subject.embed.record.record)
) {
embedHidden = hiddenPosts.includes(subject.embed.record.record.uri)
}
if (embedHidden) {
moderations.embed.filter = true
moderations.embed.blur = true
if (!moderations.embed.cause) {
moderations.embed.cause = {
// @ts-ignore Temporary extension to the moderation system -prf
type: 'post-hidden',
source: {type: 'user'},
priority: 1,
}
}
}
}
return moderations
}
+7
View File
@@ -60,6 +60,13 @@ export function describeModerationCause(
} }
} }
} }
// @ts-ignore Temporary extension to the moderation system -prf
if (cause.type === 'post-hidden') {
return {
name: 'Post Hidden by You',
description: 'You have hidden this post',
}
}
return cause.labelDef.strings[context].en return cause.labelDef.strings[context].en
} }
+152
View File
@@ -0,0 +1,152 @@
import {Platform} from 'react-native'
export type EmbedPlayerParams =
| {type: 'youtube_video'; videoId: string; playerUri: string}
| {type: 'twitch_live'; channelId: string; playerUri: string}
| {type: 'spotify_album'; albumId: string; playerUri: string}
| {
type: 'spotify_playlist'
playlistId: string
playerUri: string
}
| {type: 'spotify_song'; songId: string; playerUri: string}
| {type: 'soundcloud_track'; user: string; track: string; playerUri: string}
| {type: 'soundcloud_set'; user: string; set: string; playerUri: string}
export function parseEmbedPlayerFromUrl(
url: string,
): EmbedPlayerParams | undefined {
let urlp
try {
urlp = new URL(url)
} catch (e) {
return undefined
}
// youtube
if (urlp.hostname === 'youtu.be') {
const videoId = urlp.pathname.split('/')[1]
if (videoId) {
return {
type: 'youtube_video',
videoId,
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
}
}
}
if (urlp.hostname === 'www.youtube.com' || urlp.hostname === 'youtube.com') {
const [_, page, shortVideoId] = urlp.pathname.split('/')
const videoId =
page === 'shorts' ? shortVideoId : (urlp.searchParams.get('v') as string)
if (videoId) {
return {
type: 'youtube_video',
videoId,
playerUri: `https://www.youtube.com/embed/${videoId}?autoplay=1`,
}
}
}
// twitch
if (urlp.hostname === 'twitch.tv' || urlp.hostname === 'www.twitch.tv') {
const parent =
Platform.OS === 'web' ? window.location.hostname : 'localhost'
const parts = urlp.pathname.split('/')
if (parts.length === 2 && parts[1]) {
return {
type: 'twitch_live',
channelId: parts[1],
playerUri: `https://player.twitch.tv/?volume=0.5&!muted&autoplay&channel=${parts[1]}&parent=${parent}`,
}
}
}
// spotify
if (urlp.hostname === 'open.spotify.com') {
const [_, type, id] = urlp.pathname.split('/')
if (type && id) {
if (type === 'playlist') {
return {
type: 'spotify_playlist',
playlistId: id,
playerUri: `https://open.spotify.com/embed/playlist/${id}`,
}
}
if (type === 'album') {
return {
type: 'spotify_album',
albumId: id,
playerUri: `https://open.spotify.com/embed/album/${id}`,
}
}
if (type === 'track') {
return {
type: 'spotify_song',
songId: id,
playerUri: `https://open.spotify.com/embed/track/${id}`,
}
}
}
}
// soundcloud
if (
urlp.hostname === 'soundcloud.com' ||
urlp.hostname === 'www.soundcloud.com'
) {
const [_, user, trackOrSets, set] = urlp.pathname.split('/')
if (user && trackOrSets) {
if (trackOrSets === 'sets' && set) {
return {
type: 'soundcloud_set',
user,
set: set,
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
}
}
return {
type: 'soundcloud_track',
user,
track: trackOrSets,
playerUri: `https://w.soundcloud.com/player/?url=${url}&auto_play=true&visual=false&hide_related=true`,
}
}
}
}
export function getPlayerHeight({
type,
width,
hasThumb,
}: {
type: EmbedPlayerParams['type']
width: number
hasThumb: boolean
}) {
if (!hasThumb) return (width / 16) * 9
switch (type) {
case 'youtube_video':
case 'twitch_live':
return (width / 16) * 9
case 'spotify_album':
return 380
case 'spotify_playlist':
return 360
case 'spotify_song':
if (width <= 300) {
return 180
}
return 232
case 'soundcloud_track':
return 165
case 'soundcloud_set':
return 360
default:
return width
}
}
-29
View File
@@ -139,35 +139,6 @@ export function feedUriToHref(url: string): string {
} }
} }
export function getYoutubeVideoId(link: string): string | undefined {
let url
try {
url = new URL(link)
} catch (e) {
return undefined
}
if (
url.hostname !== 'www.youtube.com' &&
url.hostname !== 'youtube.com' &&
url.hostname !== 'youtu.be'
) {
return undefined
}
if (url.hostname === 'youtu.be') {
const videoId = url.pathname.split('/')[1]
if (!videoId) {
return undefined
}
return videoId
}
const videoId = url.searchParams.get('v') as string
if (!videoId) {
return undefined
}
return videoId
}
/** /**
* Checks if the label in the post text matches the host of the link facet. * Checks if the label in the post text matches the host of the link facet.
* *
+2 -2
View File
@@ -7,6 +7,6 @@ test('sanitizeAppLanguageSetting', () => {
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en) expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi) expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en) expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,fr')).toBe(AppLanguage.en) expect(sanitizeAppLanguageSetting('en,foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('fr,en')).toBe(AppLanguage.en) expect(sanitizeAppLanguageSetting('foo,en')).toBe(AppLanguage.en)
}) })
+8
View File
@@ -114,6 +114,14 @@ export function sanitizeAppLanguageSetting(appLanguage: string): AppLanguage {
return AppLanguage.hi return AppLanguage.hi
case 'ja': case 'ja':
return AppLanguage.ja return AppLanguage.ja
case 'fr':
return AppLanguage.fr
// DISABLED until this translation is fixed -prf
// case 'de':
// return AppLanguage.de
// DISABLED until this translation is more thoroughly reviewed -prf
// case 'es':
// return AppLanguage.es
default: default:
continue continue
} }
+20
View File
@@ -5,6 +5,12 @@ import {useLanguagePrefs} from '#/state/preferences'
import {messages as messagesEn} from '#/locale/locales/en/messages' import {messages as messagesEn} from '#/locale/locales/en/messages'
import {messages as messagesHi} from '#/locale/locales/hi/messages' import {messages as messagesHi} from '#/locale/locales/hi/messages'
import {messages as messagesJa} from '#/locale/locales/ja/messages' import {messages as messagesJa} from '#/locale/locales/ja/messages'
import {messages as messagesFr} from '#/locale/locales/fr/messages'
// DISABLED until this translation is fixed -prf
// import {messages as messagesDe} from '#/locale/locales/de/messages'
// DISABLED until this translation is more thoroughly reviewed -prf
// import {messages as messagesEs} from '#/locale/locales/es/messages'
import {sanitizeAppLanguageSetting} from '#/locale/helpers' import {sanitizeAppLanguageSetting} from '#/locale/helpers'
import {AppLanguage} from '#/locale/languages' import {AppLanguage} from '#/locale/languages'
@@ -21,6 +27,20 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesJa}) i18n.loadAndActivate({locale, messages: messagesJa})
break break
} }
case AppLanguage.fr: {
i18n.loadAndActivate({locale, messages: messagesFr})
break
}
// DISABLED until this translation is fixed -prf
// case AppLanguage.de: {
// i18n.loadAndActivate({locale, messages: messagesDe})
// break
// }
// DISABLED until this translation is more thoroughly reviewed -prf
// case AppLanguage.es: {
// i18n.loadAndActivate({locale, messages: messagesEs})
// break
// }
default: { default: {
i18n.loadAndActivate({locale, messages: messagesEn}) i18n.loadAndActivate({locale, messages: messagesEn})
break break
+14
View File
@@ -20,6 +20,20 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ja/messages`) mod = await import(`./locales/ja/messages`)
break break
} }
case AppLanguage.fr: {
mod = await import(`./locales/fr/messages`)
break
}
// DISABLED until this translation is fixed -prf
// case AppLanguage.de: {
// mod = await import(`./locales/de/messages`)
// break
// }
// DISABLED until this translation is more thoroughly reviewed -prf
// case AppLanguage.es: {
// mod = await import(`./locales/es/messages`)
// break
// }
default: { default: {
mod = await import(`./locales/en/messages`) mod = await import(`./locales/en/messages`)
break break
+10
View File
@@ -8,6 +8,11 @@ export enum AppLanguage {
en = 'en', en = 'en',
hi = 'hi', hi = 'hi',
ja = 'ja', ja = 'ja',
fr = 'fr',
// DISABLED until this translation is fixed -prf
// de = 'de',
// DISABLED until this translation is more thoroughly reviewed -prf
// es = 'es',
} }
interface AppLanguageConfig { interface AppLanguageConfig {
@@ -19,6 +24,11 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'}, {code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.hi, name: 'हिंदी'}, {code2: AppLanguage.hi, name: 'हिंदी'},
{code2: AppLanguage.ja, name: '日本語'}, {code2: AppLanguage.ja, name: '日本語'},
{code2: AppLanguage.fr, name: 'Français'},
// DISABLED until this translation is fixed -prf
// {code2: AppLanguage.de, name: 'Deutsch'},
// DISABLED until this translation is more thoroughly reviewed -prf
// {code2: AppLanguage.es, name: 'Español'},
] ]
export const LANGUAGES: Language[] = [ export const LANGUAGES: Language[] = [
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+7 -5
View File
@@ -287,15 +287,11 @@ export class Logger {
metadata: Metadata = {}, metadata: Metadata = {},
) { ) {
if (!this.enabled) return if (!this.enabled) return
if (!enabledLogLevels[this.level].includes(level)) return
const timestamp = Date.now() const timestamp = Date.now()
const meta = metadata || {} const meta = metadata || {}
for (const transport of this.transports) { // send every log to syslog
transport(level, message, meta, timestamp)
}
add({ add({
id: nanoid(), id: nanoid(),
timestamp, timestamp,
@@ -303,6 +299,12 @@ export class Logger {
message, message,
metadata: meta, metadata: meta,
}) })
if (!enabledLogLevels[this.level].includes(level)) return
for (const transport of this.transports) {
transport(level, message, meta, timestamp)
}
} }
} }
+1 -1
View File
@@ -4,7 +4,7 @@ let entries: ConsoleTransportEntry[] = []
export function add(entry: ConsoleTransportEntry) { export function add(entry: ConsoleTransportEntry) {
entries.unshift(entry) entries.unshift(entry)
entries = entries.slice(0, 50) entries = entries.slice(0, 500)
} }
export function getEntries() { export function getEntries() {
+7
View File
@@ -0,0 +1,7 @@
// HACK
// expo-modules-core tries to require('crypto') in uuid.web.js
// and while it tries to detect web crypto before doing so, our
// build fails when it tries to do this require. We use a babel
// and tsconfig alias to direct it here
// -prf
export default crypto
+4 -2
View File
@@ -14,5 +14,7 @@ export const isMobileWeb =
global.window.matchMedia(isMobileWebMediaQuery)?.matches global.window.matchMedia(isMobileWebMediaQuery)?.matches
export const deviceLocales = dedupArray( export const deviceLocales = dedupArray(
getLocales?.().map?.(locale => locale.languageCode), getLocales?.()
) .map?.(locale => locale.languageCode)
.filter(code => typeof code === 'string'),
) as string[]
+1
View File
@@ -108,6 +108,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
onboarding: { onboarding: {
step: legacy.onboarding?.step || defaults.onboarding.step, step: legacy.onboarding?.step || defaults.onboarding.step,
}, },
hiddenPosts: defaults.hiddenPosts,
} }
} }
+2
View File
@@ -37,6 +37,7 @@ export const schema = z.object({
onboarding: z.object({ onboarding: z.object({
step: z.string(), step: z.string(),
}), }),
hiddenPosts: z.array(z.string()).optional(), // should move to server
}) })
export type Schema = z.infer<typeof schema> export type Schema = z.infer<typeof schema>
@@ -66,4 +67,5 @@ export const defaults: Schema = {
onboarding: { onboarding: {
step: 'Home', step: 'Home',
}, },
hiddenPosts: [],
} }
+64
View File
@@ -0,0 +1,64 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type SetStateCb = (
s: persisted.Schema['hiddenPosts'],
) => persisted.Schema['hiddenPosts']
type StateContext = persisted.Schema['hiddenPosts']
type ApiContext = {
hidePost: ({uri}: {uri: string}) => void
unhidePost: ({uri}: {uri: string}) => void
}
const stateContext = React.createContext<StateContext>(
persisted.defaults.hiddenPosts,
)
const apiContext = React.createContext<ApiContext>({
hidePost: () => {},
unhidePost: () => {},
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const [state, setState] = React.useState(persisted.get('hiddenPosts'))
const setStateWrapped = React.useCallback(
(fn: SetStateCb) => {
const s = fn(persisted.get('hiddenPosts'))
setState(s)
persisted.write('hiddenPosts', s)
},
[setState],
)
const api = React.useMemo(
() => ({
hidePost: ({uri}: {uri: string}) => {
setStateWrapped(s => [...(s || []), uri])
},
unhidePost: ({uri}: {uri: string}) => {
setStateWrapped(s => (s || []).filter(u => u !== uri))
},
}),
[setStateWrapped],
)
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('hiddenPosts'))
})
}, [setStateWrapped])
return (
<stateContext.Provider value={state}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
</stateContext.Provider>
)
}
export function useHiddenPosts() {
return React.useContext(stateContext)
}
export function useHiddenPostsApi() {
return React.useContext(apiContext)
}
+5 -1
View File
@@ -1,17 +1,21 @@
import React from 'react' import React from 'react'
import {Provider as LanguagesProvider} from './languages' import {Provider as LanguagesProvider} from './languages'
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required' import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts'
export {useLanguagePrefs, useLanguagePrefsApi} from './languages' export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
export { export {
useRequireAltTextEnabled, useRequireAltTextEnabled,
useSetRequireAltTextEnabled, useSetRequireAltTextEnabled,
} from './alt-text-required' } from './alt-text-required'
export * from './hidden-posts'
export function Provider({children}: React.PropsWithChildren<{}>) { export function Provider({children}: React.PropsWithChildren<{}>) {
return ( return (
<LanguagesProvider> <LanguagesProvider>
<AltTextRequiredProvider>{children}</AltTextRequiredProvider> <AltTextRequiredProvider>
<HiddenPostsProvider>{children}</HiddenPostsProvider>
</AltTextRequiredProvider>
</LanguagesProvider> </LanguagesProvider>
) )
} }
+3 -2
View File
@@ -11,6 +11,7 @@ import {
getModerationOpts, getModerationOpts,
useModerationOpts, useModerationOpts,
} from './preferences' } from './preferences'
import {isInvalidHandle} from '#/lib/strings/handles'
const DEFAULT_MOD_OPTS = getModerationOpts({ const DEFAULT_MOD_OPTS = getModerationOpts({
userDid: '', userDid: '',
@@ -111,7 +112,7 @@ function computeSuggestions(
} }
return items.filter(profile => { return items.filter(profile => {
const mod = moderateProfile(profile, moderationOpts) const mod = moderateProfile(profile, moderationOpts)
return !mod.account.filter return !mod.account.filter && mod.account.cause?.type !== 'muted'
}) })
} }
@@ -119,7 +120,7 @@ function prefixMatch(
prefix: string, prefix: string,
info: AppBskyActorDefs.ProfileViewBasic, info: AppBskyActorDefs.ProfileViewBasic,
): boolean { ): boolean {
if (info.handle.includes(prefix)) { if (!isInvalidHandle(info.handle) && info.handle.includes(prefix)) {
return true return true
} }
if (info.displayName?.toLocaleLowerCase().includes(prefix)) { if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
+5 -1
View File
@@ -218,11 +218,13 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
export function usePinnedFeedsInfos(): { export function usePinnedFeedsInfos(): {
feeds: FeedSourceInfo[] feeds: FeedSourceInfo[]
hasPinnedCustom: boolean hasPinnedCustom: boolean
isLoading: boolean
} { } {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([ const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
FOLLOWING_FEED_STUB, FOLLOWING_FEED_STUB,
]) ])
const [isLoading, setLoading] = React.useState(true)
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const hasPinnedCustom = React.useMemo<boolean>(() => { const hasPinnedCustom = React.useMemo<boolean>(() => {
@@ -249,6 +251,7 @@ export function usePinnedFeedsInfos(): {
// these requests can fail, need to filter those out // these requests can fail, need to filter those out
try { try {
return await queryClient.fetchQuery({ return await queryClient.fetchQuery({
staleTime: STALE.SECONDS.FIFTEEN,
queryKey: feedSourceInfoQueryKey({uri}), queryKey: feedSourceInfoQueryKey({uri}),
queryFn: async () => { queryFn: async () => {
const type = getFeedTypeFromUri(uri) const type = getFeedTypeFromUri(uri)
@@ -283,10 +286,11 @@ export function usePinnedFeedsInfos(): {
) as FeedSourceInfo[] ) as FeedSourceInfo[]
setTabs([FOLLOWING_FEED_STUB].concat(views)) setTabs([FOLLOWING_FEED_STUB].concat(views))
setLoading(false)
} }
fetchFeedInfo() fetchFeedInfo()
}, [queryClient, setTabs, preferences?.feeds?.pinned]) }, [queryClient, setTabs, preferences?.feeds?.pinned])
return {feeds: tabs, hasPinnedCustom} return {feeds: tabs, hasPinnedCustom, isLoading}
} }
@@ -89,6 +89,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
// update & broadcast // update & broadcast
setNumUnread('') setNumUnread('')
broadcast.postMessage({event: ''}) broadcast.postMessage({event: ''})
if (isNative) {
Notifications.setBadgeCountAsync(0)
}
}, },
async checkUnread({invalidate}: {invalidate?: boolean} = {}) { async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
+4 -2
View File
@@ -2,12 +2,12 @@ import {
AppBskyNotificationListNotifications, AppBskyNotificationListNotifications,
ModerationOpts, ModerationOpts,
moderateProfile, moderateProfile,
moderatePost,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AppBskyFeedRepost, AppBskyFeedRepost,
AppBskyFeedLike, AppBskyFeedLike,
} from '@atproto/api' } from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import chunk from 'lodash.chunk' import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query' import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session' import {getAgent} from '../../session'
@@ -156,7 +156,7 @@ async function fetchSubjects(
): Promise<Map<string, AppBskyFeedDefs.PostView>> { ): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>() const uris = new Set<string>()
for (const notif of groupedNotifs) { for (const notif of groupedNotifs) {
if (notif.subjectUri) { if (notif.subjectUri && !notif.subjectUri.includes('feed.generator')) {
uris.add(notif.subjectUri) uris.add(notif.subjectUri)
} }
} }
@@ -216,6 +216,8 @@ function getSubjectUri(
? notif.record.subject?.uri ? notif.record.subject?.uri
: undefined : undefined
} }
} else if (type === 'feedgen-like') {
return notif.reasonSubject
} }
} }
+2 -6
View File
@@ -1,10 +1,5 @@
import React, {useCallback, useEffect, useRef} from 'react' import React, {useCallback, useEffect, useRef} from 'react'
import { import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
AppBskyFeedDefs,
AppBskyFeedPost,
moderatePost,
PostModeration,
} from '@atproto/api'
import { import {
useInfiniteQuery, useInfiniteQuery,
InfiniteData, InfiniteData,
@@ -12,6 +7,7 @@ import {
QueryClient, QueryClient,
useQueryClient, useQueryClient,
} from '@tanstack/react-query' } from '@tanstack/react-query'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useFeedTuners} from '../preferences/feed-tuners' import {useFeedTuners} from '../preferences/feed-tuners'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip' import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types' import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
+9 -2
View File
@@ -19,6 +19,7 @@ import {
} from '#/state/queries/preferences/const' } from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation' import {getModerationOpts} from '#/state/queries/preferences/moderation'
import {STALE} from '#/state/queries' import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
export * from '#/state/queries/preferences/types' export * from '#/state/queries/preferences/types'
export * from '#/state/queries/preferences/moderation' export * from '#/state/queries/preferences/moderation'
@@ -94,15 +95,21 @@ export function usePreferencesQuery() {
export function useModerationOpts() { export function useModerationOpts() {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const prefs = usePreferencesQuery() const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => { const opts = useMemo(() => {
if (!prefs.data) { if (!prefs.data) {
return return
} }
return getModerationOpts({ const moderationOpts = getModerationOpts({
userDid: currentAccount?.did || '', userDid: currentAccount?.did || '',
preferences: prefs.data, preferences: prefs.data,
}) })
}, [currentAccount?.did, prefs.data])
return {
...moderationOpts,
hiddenPosts,
}
}, [currentAccount?.did, prefs.data, hiddenPosts])
return opts return opts
} }
+2
View File
@@ -189,6 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, },
logger.DebugContext.session, logger.DebugContext.session,
) )
track('Try Create Account')
const agent = new BskyAgent({service}) const agent = new BskyAgent({service})
@@ -231,6 +232,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
}, },
logger.DebugContext.session, logger.DebugContext.session,
) )
track('Create Account')
}, },
[upsertAccount, queryClient], [upsertAccount, queryClient],
) )
+44 -3
View File
@@ -3,8 +3,9 @@ import {View, Pressable} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useNavigation} from '@react-navigation/native'
import {isIOS} from 'platform/detection' import {isIOS, isNative} from 'platform/detection'
import {Login} from 'view/com/auth/login/Login' import {Login} from 'view/com/auth/login/Login'
import {CreateAccount} from 'view/com/auth/create/CreateAccount' import {CreateAccount} from 'view/com/auth/create/CreateAccount'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary' import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -18,6 +19,9 @@ import {
useLoggedOutView, useLoggedOutView,
useLoggedOutViewControls, useLoggedOutViewControls,
} from '#/state/shell/logged-out' } from '#/state/shell/logged-out'
import {useSession} from '#/state/session'
import {Text} from '#/view/com/util/text/Text'
import {NavigationProp} from 'lib/routes/types'
enum ScreenState { enum ScreenState {
S_LoginOrCreateAccount, S_LoginOrCreateAccount,
@@ -26,6 +30,7 @@ enum ScreenState {
} }
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {hasSession} = useSession()
const {_} = useLingui() const {_} = useLingui()
const pal = usePalette('default') const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
@@ -40,6 +45,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
) )
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const {clearRequestedAccount} = useLoggedOutViewControls() const {clearRequestedAccount} = useLoggedOutViewControls()
const navigation = useNavigation<NavigationProp>()
const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
React.useEffect(() => { React.useEffect(() => {
screen('Login') screen('Login')
@@ -53,6 +60,10 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
clearRequestedAccount() clearRequestedAccount()
}, [clearRequestedAccount, onDismiss]) }, [clearRequestedAccount, onDismiss])
const onPressSearch = React.useCallback(() => {
navigation.navigate(`SearchTab`)
}, [navigation])
return ( return (
<View <View
testID="noSessionView" testID="noSessionView"
@@ -65,7 +76,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}, },
]}> ]}>
<ErrorBoundary> <ErrorBoundary>
{onDismiss && ( {onDismiss ? (
<Pressable <Pressable
accessibilityHint={_(msg`Go back`)} accessibilityHint={_(msg`Go back`)}
accessibilityLabel={_(msg`Go back`)} accessibilityLabel={_(msg`Go back`)}
@@ -88,7 +99,37 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}} }}
/> />
</Pressable> </Pressable>
)} ) : isNative && !hasSession && isFirstScreen ? (
<Pressable
accessibilityHint={_(msg`Search for users`)}
accessibilityLabel={_(msg`Search for users`)}
accessibilityRole="button"
style={{
flexDirection: 'row',
alignItems: 'center',
gap: 4,
position: 'absolute',
top: 20,
right: 20,
paddingHorizontal: 16,
paddingVertical: 8,
zIndex: 100,
backgroundColor: pal.btn.backgroundColor,
borderRadius: 100,
}}
onPress={onPressSearch}>
<Text type="lg-bold" style={[pal.text]}>
Search{' '}
</Text>
<FontAwesomeIcon
icon="search"
size={16}
style={{
color: String(pal.text.color),
}}
/>
</Pressable>
) : null}
{screenState === ScreenState.S_LoginOrCreateAccount ? ( {screenState === ScreenState.S_LoginOrCreateAccount ? (
<SplashScreen <SplashScreen
+4 -9
View File
@@ -1,7 +1,6 @@
import React from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
KeyboardAvoidingView,
ScrollView, ScrollView,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
@@ -28,9 +27,10 @@ import {IS_PROD} from '#/lib/constants'
import {Step1} from './Step1' import {Step1} from './Step1'
import {Step2} from './Step2' import {Step2} from './Step2'
import {Step3} from './Step3' import {Step3} from './Step3'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
export function CreateAccount({onPressBack}: {onPressBack: () => void}) { export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {track, screen} = useAnalytics() const {screen} = useAnalytics()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const [uiState, uiDispatch] = useCreateAccount() const [uiState, uiDispatch] = useCreateAccount()
@@ -38,6 +38,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {createAccount} = useSessionApi() const {createAccount} = useSessionApi()
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation() const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation() const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
const {isTabletOrDesktop} = useWebMediaQueries()
React.useEffect(() => { React.useEffect(() => {
screen('CreateAccount') screen('CreateAccount')
@@ -93,21 +94,17 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
uiDispatch, uiDispatch,
_, _,
}) })
track('Create Account')
setBirthDate({birthDate: uiState.birthDate}) setBirthDate({birthDate: uiState.birthDate})
if (IS_PROD(uiState.serviceUrl)) { if (IS_PROD(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS) setSavedFeeds(DEFAULT_PROD_FEEDS)
} }
} catch { } catch {
// dont need to handle here // dont need to handle here
} finally {
track('Try Create Account')
} }
} }
}, [ }, [
uiState, uiState,
uiDispatch, uiDispatch,
track,
onboardingDispatch, onboardingDispatch,
createAccount, createAccount,
setBirthDate, setBirthDate,
@@ -124,7 +121,6 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
title={_(msg`Create Account`)} title={_(msg`Create Account`)}
description={_(msg`We're so excited to have you join us!`)}> description={_(msg`We're so excited to have you join us!`)}>
<ScrollView testID="createAccount" style={pal.view}> <ScrollView testID="createAccount" style={pal.view}>
<KeyboardAvoidingView behavior="padding">
<View style={styles.stepContainer}> <View style={styles.stepContainer}>
{uiState.step === 1 && ( {uiState.step === 1 && (
<Step1 uiState={uiState} uiDispatch={uiDispatch} /> <Step1 uiState={uiState} uiDispatch={uiDispatch} />
@@ -180,8 +176,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
</> </>
) : undefined} ) : undefined}
</View> </View>
<View style={s.footerSpacer} /> <View style={{height: isTabletOrDesktop ? 50 : 400}} />
</KeyboardAvoidingView>
</ScrollView> </ScrollView>
</LoggedOutLayout> </LoggedOutLayout>
) )
+26 -1
View File
@@ -13,6 +13,17 @@ import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {logger} from '#/logger'
function sanitizeDate(date: Date): Date {
if (!date || date.toString() === 'Invalid Date') {
logger.error(`Create account: handled invalid date for birthDate`, {
hasDate: !!date,
})
return new Date()
}
return date
}
/** STEP 2: Your account /** STEP 2: Your account
* @field Invite code or waitlist * @field Invite code or waitlist
@@ -38,6 +49,10 @@ export function Step2({
openModal({name: 'waitlist'}) openModal({name: 'waitlist'})
}, [openModal]) }, [openModal])
const birthDate = React.useMemo(() => {
return sanitizeDate(uiState.birthDate)
}, [uiState.birthDate])
return ( return (
<View> <View>
<StepHeader step="2" title={_(msg`Your account`)} /> <StepHeader step="2" title={_(msg`Your account`)} />
@@ -56,6 +71,9 @@ export function Step2({
onChange={value => uiDispatch({type: 'set-invite-code', value})} onChange={value => uiDispatch({type: 'set-invite-code', value})}
accessibilityLabel={_(msg`Invite code`)} accessibilityLabel={_(msg`Invite code`)}
accessibilityHint="Input invite code to proceed" accessibilityHint="Input invite code to proceed"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/> />
</View> </View>
)} )}
@@ -90,6 +108,9 @@ export function Step2({
accessibilityLabel={_(msg`Email`)} accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input email for Bluesky waitlist" accessibilityHint="Input email for Bluesky waitlist"
accessibilityLabelledBy="email" accessibilityLabelledBy="email"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/> />
</View> </View>
@@ -111,6 +132,9 @@ export function Step2({
accessibilityLabel={_(msg`Password`)} accessibilityLabel={_(msg`Password`)}
accessibilityHint="Set password" accessibilityHint="Set password"
accessibilityLabelledBy="password" accessibilityLabelledBy="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/> />
</View> </View>
@@ -122,8 +146,9 @@ export function Step2({
<Trans>Your birth date</Trans> <Trans>Your birth date</Trans>
</Text> </Text>
<DateInput <DateInput
handleAsUTC
testID="birthdayInput" testID="birthdayInput"
value={uiState.birthDate} value={birthDate}
onChange={value => uiDispatch({type: 'set-birth-date', value})} onChange={value => uiDispatch({type: 'set-birth-date', value})}
buttonType="default-light" buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]} buttonStyle={[pal.border, styles.dateInputButton]}
+1 -1
View File
@@ -144,7 +144,7 @@ export async function submit({
} }
export function is13(state: CreateAccountState) { export function is13(state: CreateAccountState) {
return getAge(state.birthDate) >= 18 return getAge(state.birthDate) >= 13
} }
export function is18(state: CreateAccountState) { export function is18(state: CreateAccountState) {
+1
View File
@@ -174,6 +174,7 @@ export const LoginForm = ({
autoCorrect={false} autoCorrect={false}
autoComplete="username" autoComplete="username"
returnKeyType="next" returnKeyType="next"
textContentType="username"
onSubmitEditing={() => { onSubmitEditing={() => {
passwordInputRef.current?.focus() passwordInputRef.current?.focus()
}} }}
+7 -3
View File
@@ -207,7 +207,11 @@ export const ComposePost = observer(function ComposePost({
setError('') setError('')
if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) { if (richtext.text.trim().length === 0 && gallery.isEmpty && !extLink) {
setError('Did you want to say anything?') setError(_(msg`Did you want to say anything?`))
return
}
if (extLink?.isLoading) {
setError(_(msg`Please wait for your link card to finish loading`))
return return
} }
@@ -438,7 +442,7 @@ export const ComposePost = observer(function ComposePost({
accessibilityLabel={_(msg`Add link card`)} accessibilityLabel={_(msg`Add link card`)}
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}> accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
<Text style={pal.text}> <Text style={pal.text}>
<Trans>Add link card:</Trans> <Trans>Add link card:</Trans>{' '}
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text> <Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@@ -452,7 +456,7 @@ export const ComposePost = observer(function ComposePost({
<OpenCameraBtn gallery={gallery} /> <OpenCameraBtn gallery={gallery} />
</> </>
) : null} ) : null}
{isDesktop ? <EmojiPickerButton /> : null} {!isMobile ? <EmojiPickerButton /> : null}
<View style={s.flex1} /> <View style={s.flex1} />
<SelectLangBtn /> <SelectLangBtn />
<CharProgress count={graphemeLength} /> <CharProgress count={graphemeLength} />
@@ -215,7 +215,13 @@ export const TextInput = forwardRef(function TextInputImpl(
autoFocus={true} autoFocus={true}
allowFontScaling allowFontScaling
multiline multiline
style={[pal.text, styles.textInput, styles.textInputFormatting]} numberOfLines={4}
style={[
pal.text,
styles.textInput,
styles.textInputFormatting,
{textAlignVertical: 'top'},
]}
{...props}> {...props}>
{textDecorated} {textDecorated}
</PasteInput> </PasteInput>
@@ -134,7 +134,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
return true return true
} }
if (event.key === 'Enter') { if (event.key === 'Enter' || event.key === 'Tab') {
enterHandler() enterHandler()
return true return true
} }
@@ -76,7 +76,7 @@ export function EmojiPicker({close}: {close: () => void}) {
return (await import('./EmojiPickerData.json')).default return (await import('./EmojiPickerData.json')).default
}} }}
onEmojiSelect={onInsert} onEmojiSelect={onInsert}
autoFocus={false} autoFocus={true}
/> />
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
@@ -96,6 +96,7 @@ const styles = StyleSheet.create({
}, },
trigger: { trigger: {
backgroundColor: 'transparent', backgroundColor: 'transparent',
// @ts-ignore web only -prf
border: 'none', border: 'none',
paddingTop: 4, paddingTop: 4,
paddingLeft: 12, paddingLeft: 12,
+2 -17
View File
@@ -1,12 +1,5 @@
import React from 'react' import React from 'react'
import { import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List' import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard' import {FeedSourceCardLoaded} from './FeedSourceCard'
@@ -180,22 +173,14 @@ export const ProfileFeedgens = React.forwardRef<
data={items} data={items}
keyExtractor={(item: any) => item._reactKey || item.uri} keyExtractor={(item: any) => item._reactKey || item.uri}
renderItem={renderItemInner} renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} headerOffset={headerOffset}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
@@ -320,6 +320,7 @@ const ImageItem = ({
accessibilityLabel={imageSrc.alt} accessibilityLabel={imageSrc.alt}
accessibilityHint="" accessibilityHint=""
onLoad={() => setIsLoaded(true)} onLoad={() => setIsLoaded(true)}
cachePolicy="memory"
/> />
</GestureDetector> </GestureDetector>
</Animated.View> </Animated.View>
+2 -12
View File
@@ -2,7 +2,6 @@ import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Dimensions, Dimensions,
RefreshControl,
StyleProp, StyleProp,
View, View,
ViewStyle, ViewStyle,
@@ -15,7 +14,6 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {ProfileCard} from '../profile/ProfileCard' import {ProfileCard} from '../profile/ProfileCard'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members' import {useListMembersQuery} from '#/state/queries/list-members'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -51,7 +49,6 @@ export function ListMembers({
headerOffset?: number headerOffset?: number
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
const pal = usePalette('default')
const {track} = useAnalytics() const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
@@ -183,6 +180,7 @@ export function ListMembers({
profile={(item as AppBskyGraphDefs.ListItemView).subject} profile={(item as AppBskyGraphDefs.ListItemView).subject}
renderButton={renderMemberButton} renderButton={renderMemberButton}
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
noModFilter
/> />
) )
}, },
@@ -215,24 +213,16 @@ export function ListMembers({
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={renderHeader} ListHeaderComponent={renderHeader}
ListFooterComponent={Footer} ListFooterComponent={Footer}
refreshControl={
<RefreshControl
refreshing={isRefreshing} refreshing={isRefreshing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} headerOffset={headerOffset}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight={desktopFixedHeightOffset || true} desktopFixedHeight={desktopFixedHeightOffset || true}
/> />
+22 -2
View File
@@ -119,11 +119,11 @@ export function MyLists({
[error, onRefresh, renderItem, pal], [error, onRefresh, renderItem, pal],
) )
const FlatListCom = inline ? RNFlatList : List if (inline) {
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
{items.length > 0 && ( {items.length > 0 && (
<FlatListCom <RNFlatList
testID={testID ? `${testID}-flatlist` : undefined} testID={testID ? `${testID}-flatlist` : undefined}
data={items} data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)} keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
@@ -144,6 +144,26 @@ export function MyLists({
)} )}
</View> </View>
) )
} else {
return (
<View testID={testID} style={style}>
{items.length > 0 && (
<List
testID={testID ? `${testID}-flatlist` : undefined}
data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
renderItem={renderItemInner}
refreshing={isPTRing}
onRefresh={onRefresh}
contentContainerStyle={[s.contentContainer]}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
}
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
+2 -17
View File
@@ -1,12 +1,5 @@
import React from 'react' import React from 'react'
import { import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {useQueryClient} from '@tanstack/react-query' import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List' import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard' import {ListCard} from './ListCard'
@@ -182,22 +175,14 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
data={items} data={items}
keyExtractor={(item: any) => item._reactKey} keyExtractor={(item: any) => item._reactKey}
renderItem={renderItemInner} renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} headerOffset={headerOffset}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
onEndReached={onEndReached} onEndReached={onEndReached}
+1
View File
@@ -80,6 +80,7 @@ export function Component({image}: Props) {
source={{ source={{
uri: image.cropped?.path ?? image.path, uri: image.cropped?.path ?? image.path,
}} }}
contentFit="contain"
accessible={true} accessible={true}
accessibilityIgnoresInvertColors accessibilityIgnoresInvertColors
/> />
+3 -3
View File
@@ -62,17 +62,17 @@ export function Component(props: ReportComponentProps) {
<Text <Text
type="2xl-bold" type="2xl-bold"
style={[pal.text, s.textCenter, {paddingBottom: 8}]}> style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
<Trans>Appeal Decision</Trans> <Trans>Appeal Content Warning</Trans>
</Text> </Text>
<ScrollView> <ScrollView>
<View style={[pal.btn, styles.detailsInputContainer]}> <View style={[pal.btn, styles.detailsInputContainer]}>
<TextInput <TextInput
accessibilityLabel={_(msg`Text input field`)} accessibilityLabel={_(msg`Text input field`)}
accessibilityHint={_( accessibilityHint={_(
msg`Please tell us why you think this decision was incorrect.`, msg`Please tell us why you think this content warning was incorrectly applied!`,
)} )}
placeholder={_( placeholder={_(
msg`Please tell us why you think this decision was incorrect.`, msg`Please tell us why you think this content warning was incorrectly applied!`,
)} )}
placeholderTextColor={pal.textLight.color} placeholderTextColor={pal.textLight.color}
value={details} value={details}
+2 -1
View File
@@ -23,7 +23,7 @@ import {
} from '#/state/queries/preferences' } from '#/state/queries/preferences'
import {logger} from '#/logger' import {logger} from '#/logger'
export const snapPoints = ['50%'] export const snapPoints = ['50%', '90%']
function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
const pal = usePalette('default') const pal = usePalette('default')
@@ -63,6 +63,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
<View> <View>
<DateInput <DateInput
handleAsUTC
testID="birthdayInput" testID="birthdayInput"
value={date} value={date}
onChange={setDate} onChange={setDate}
+1 -1
View File
@@ -82,7 +82,7 @@ export function ModalsContainer() {
useEffect(() => { useEffect(() => {
if (isModalActive) { if (isModalActive) {
bottomSheetRef.current?.expand() bottomSheetRef.current?.snapToIndex(0)
} else { } else {
bottomSheetRef.current?.close() bottomSheetRef.current?.close()
} }
+2 -2
View File
@@ -69,7 +69,7 @@ export function Component({
<ScrollView> <ScrollView>
<Text style={[pal.text, styles.description]}> <Text style={[pal.text, styles.description]}>
Choose "Everybody" or "Nobody" <Trans>Choose "Everybody" or "Nobody"</Trans>
</Text> </Text>
<View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}> <View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
<Selectable <Selectable
@@ -86,7 +86,7 @@ export function Component({
/> />
</View> </View>
<Text style={[pal.text, styles.description]}> <Text style={[pal.text, styles.description]}>
Or combine these options: <Trans>Or combine these options:</Trans>
</Text> </Text>
<View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}> <View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
<Selectable <Selectable
@@ -42,6 +42,7 @@ export function InputIssueDetails({
accessibilityHint="Add more details to your report"> accessibilityHint="Add more details to your report">
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} /> <FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
<Text style={[pal.text, s.f18, pal.link]}> <Text style={[pal.text, s.f18, pal.link]}>
{' '}
<Trans>Back</Trans> <Trans>Back</Trans>
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
+3 -3
View File
@@ -44,9 +44,9 @@ export function Component(content: ReportComponentProps) {
const {isMobile} = useWebMediaQueries() const {isMobile} = useWebMediaQueries()
const [isProcessing, setIsProcessing] = useState(false) const [isProcessing, setIsProcessing] = useState(false)
const [showDetailsInput, setShowDetailsInput] = useState(false) const [showDetailsInput, setShowDetailsInput] = useState(false)
const [error, setError] = useState<string>() const [error, setError] = useState<string>('')
const [issue, setIssue] = useState<string>() const [issue, setIssue] = useState<string>('')
const [details, setDetails] = useState<string>() const [details, setDetails] = useState<string>('')
const isAccountReport = 'did' in content const isAccountReport = 'did' in content
const subjectKey = isAccountReport ? content.did : content.uri const subjectKey = isAccountReport ? content.did : content.uri
const atUri = useMemo( const atUri = useMemo(
+1 -9
View File
@@ -1,13 +1,12 @@
import React from 'react' import React from 'react'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {FeedItem} from './FeedItem' import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState' import {EmptyState} from '../util/EmptyState'
import {s} from 'lib/styles' import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -30,7 +29,6 @@ export function Feed({
onScrolledDownChange: (isScrolledDown: boolean) => void onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
}) { }) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
@@ -152,14 +150,8 @@ export function Feed({
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={0.6} onEndReachedThreshold={0.6}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
+16 -2
View File
@@ -42,6 +42,7 @@ import {TimeElapsed} from '../util/TimeElapsed'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro' import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {FeedSourceCard} from '../feeds/FeedSourceCard'
const MAX_AUTHORS = 5 const MAX_AUTHORS = 5
@@ -112,7 +113,7 @@ let FeedItem = ({
] ]
}, [item, moderationOpts]) }, [item, moderationOpts])
if (item.subjectUri && !item.subject) { if (item.subjectUri && !item.subject && item.type !== 'feedgen-like') {
// don't render anything if the target post was deleted or unfindable // don't render anything if the target post was deleted or unfindable
return <View /> return <View />
} }
@@ -166,7 +167,7 @@ let FeedItem = ({
iconStyle = [s.blue3 as FontAwesomeIconStyle] iconStyle = [s.blue3 as FontAwesomeIconStyle]
} else if (item.type === 'feedgen-like') { } else if (item.type === 'feedgen-like') {
action = `liked your custom feed${ action = `liked your custom feed${
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}}'` : '' item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : ''
}` }`
icon = 'HeartIconSolid' icon = 'HeartIconSolid'
iconStyle = [ iconStyle = [
@@ -256,6 +257,13 @@ let FeedItem = ({
{item.type === 'post-like' || item.type === 'repost' ? ( {item.type === 'post-like' || item.type === 'repost' ? (
<AdditionalPostText post={item.subject} /> <AdditionalPostText post={item.subject} />
) : null} ) : null}
{item.type === 'feedgen-like' && item.subjectUri ? (
<FeedSourceCard
feedUri={item.subjectUri}
style={[pal.view, pal.border, styles.feedcard]}
showLikes
/>
) : null}
</View> </View>
</Link> </Link>
) )
@@ -496,6 +504,12 @@ const styles = StyleSheet.create({
marginLeft: 2, marginLeft: 2,
opacity: 0.8, opacity: 0.8,
}, },
feedcard: {
borderWidth: 1,
borderRadius: 8,
paddingVertical: 12,
marginTop: 6,
},
addedContainer: { addedContainer: {
paddingTop: 4, paddingTop: 4,
+1 -9
View File
@@ -1,18 +1,16 @@
import React, {useCallback, useMemo, useState} from 'react' import React, {useCallback, useMemo, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api' import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostLikedByQuery} from '#/state/queries/post-liked-by' import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function PostLikedBy({uri}: {uri: string}) { export function PostLikedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
@@ -88,14 +86,8 @@ export function PostLikedBy({uri}: {uri: string}) {
<List <List
data={likes} data={likes}
keyExtractor={item => item.actor.did} keyExtractor={item => item.actor.did}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+1 -9
View File
@@ -1,18 +1,16 @@
import React, {useMemo, useCallback, useState} from 'react' import React, {useMemo, useCallback, useState} from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard' import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri' import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by' import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function PostRepostedBy({uri}: {uri: string}) { export function PostRepostedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data: resolvedUri, data: resolvedUri,
@@ -89,14 +87,8 @@ export function PostRepostedBy({uri}: {uri: string}) {
<List <List
data={repostedBy} data={repostedBy}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
-7
View File
@@ -2,7 +2,6 @@ import React, {useEffect, useRef} from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
Pressable, Pressable,
RefreshControl,
StyleSheet, StyleSheet,
TouchableOpacity, TouchableOpacity,
View, View,
@@ -349,14 +348,8 @@ function PostThreadLoaded({
} }
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
renderItem={renderItem} renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onPTR} onRefresh={onPTR}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onContentSizeChange={onContentSizeChange} onContentSizeChange={onContentSizeChange}
style={s.hContentRegion} style={s.hContentRegion}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
+7 -11
View File
@@ -5,9 +5,9 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
RichText as RichTextAPI, RichText as RichTextAPI,
moderatePost,
PostModeration, PostModeration,
} from '@atproto/api' } from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Link, TextLink} from '../util/Link' import {Link, TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
@@ -42,7 +42,6 @@ import {useComposerControls} from '#/state/shell/composer'
import {useModerationOpts} from '#/state/queries/preferences' import {useModerationOpts} from '#/state/queries/preferences'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {ThreadPost} from '#/state/queries/post-thread' import {ThreadPost} from '#/state/queries/post-thread'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply' import {WhoCanReply} from '../threadgate/WhoCanReply'
@@ -187,9 +186,9 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by') return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author]) }, [post.uri, post.author])
const repostsTitle = 'Reposts of this post' const repostsTitle = 'Reposts of this post'
const isSelfLabeledPost = const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' && moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src === currentAccount?.did moderation.decisions.post.cause.label.src !== currentAccount?.did
const translatorUrl = getTranslatorLink( const translatorUrl = getTranslatorLink(
record?.text || '', record?.text || '',
@@ -335,6 +334,9 @@ let PostThreadItemLoaded = ({
postCid={post.cid} postCid={post.cid}
postUri={post.uri} postUri={post.uri}
record={record} record={record}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
style={{ style={{
paddingVertical: 6, paddingVertical: 6,
paddingHorizontal: 10, paddingHorizontal: 10,
@@ -354,13 +356,6 @@ let PostThreadItemLoaded = ({
includeMute includeMute
style={styles.alert} style={styles.alert}
/> />
{post.author.did === currentAccount?.did && !isSelfLabeledPost ? (
<LabelInfo
details={{uri: post.uri, cid: post.cid}}
labels={post.labels}
style={{marginBottom: 8}}
/>
) : null}
{richText?.text ? ( {richText?.text ? (
<View <View
style={[ style={[
@@ -544,6 +539,7 @@ let PostThreadItemLoaded = ({
timestamp={post.indexedAt} timestamp={post.indexedAt}
postHref={postHref} postHref={postHref}
showAvatar={isThreadedChild} showAvatar={isThreadedChild}
avatarModeration={moderation.avatar}
avatarSize={28} avatarSize={28}
displayNameType="md-bold" displayNameType="md-bold"
displayNameStyle={isThreadedChild && s.ml2} displayNameStyle={isThreadedChild && s.ml2}
+2 -1
View File
@@ -4,10 +4,10 @@ import {
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
moderatePost,
PostModeration, PostModeration,
RichText as RichTextAPI, RichText as RichTextAPI,
} from '@atproto/api' } from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Link, TextLink} from '../util/Link' import {Link, TextLink} from '../util/Link'
import {UserInfoText} from '../util/UserInfoText' import {UserInfoText} from '../util/UserInfoText'
@@ -221,6 +221,7 @@ const styles = StyleSheet.create({
paddingBottom: 5, paddingBottom: 5,
paddingLeft: 10, paddingLeft: 10,
borderTopWidth: 1, borderTopWidth: 1,
// @ts-ignore web only -prf
cursor: 'pointer', cursor: 'pointer',
}, },
layout: { layout: {
+5 -13
View File
@@ -3,7 +3,6 @@ import {
ActivityIndicator, ActivityIndicator,
AppState, AppState,
Dimensions, Dimensions,
RefreshControl,
StyleProp, StyleProp,
StyleSheet, StyleSheet,
View, View,
@@ -16,7 +15,6 @@ import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice' import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext' import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import { import {
@@ -74,7 +72,6 @@ let Feed = ({
ListHeaderComponent?: () => JSX.Element ListHeaderComponent?: () => JSX.Element
extraData?: any extraData?: any
}): React.ReactNode => { }): React.ReactNode => {
const pal = usePalette('default')
const theme = useTheme() const theme = useTheme()
const {track} = useAnalytics() const {track} = useAnalytics()
const queryClient = useQueryClient() const queryClient = useQueryClient()
@@ -98,10 +95,13 @@ let Feed = ({
isFetchingNextPage, isFetchingNextPage,
fetchNextPage, fetchNextPage,
} = usePostFeedQuery(feed, feedParams, opts) } = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
if (data?.pages[0]) { if (data?.pages[0]) {
lastFetchRef.current = data?.pages[0].fetchedAt lastFetchRef.current = data?.pages[0].fetchedAt
} }
const isEmpty = React.useMemo(
() => !isFetching && !data?.pages?.some(page => page.slices.length),
[isFetching, data],
)
const checkForNew = React.useCallback(async () => { const checkForNew = React.useCallback(async () => {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) { if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
@@ -294,25 +294,17 @@ let Feed = ({
renderItem={renderItem} renderItem={renderItem}
ListFooterComponent={FeedFooter} ListFooterComponent={FeedFooter}
ListHeaderComponent={ListHeaderComponent} ListHeaderComponent={ListHeaderComponent}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} headerOffset={headerOffset}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={{ contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5, minHeight: Dimensions.get('window').height * 1.5,
}} }}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange} onScrolledDownChange={onScrolledDownChange}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached} onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more onEndReachedThreshold={2} // number of posts left to trigger load more
removeClippedSubviews={true} removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
extraData={extraData} extraData={extraData}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight={ desktopFixedHeight={
+14 -1
View File
@@ -34,6 +34,7 @@ import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow' import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {FeedNameText} from '../util/FeedInfoText' import {FeedNameText} from '../util/FeedInfoText'
import {useSession} from '#/state/session'
export function FeedItem({ export function FeedItem({
post, post,
@@ -102,10 +103,14 @@ let FeedItemInner = ({
}): React.ReactNode => { }): React.ReactNode => {
const {openComposer} = useComposerControls() const {openComposer} = useComposerControls()
const pal = usePalette('default') const pal = usePalette('default')
const {currentAccount} = useSession()
const href = useMemo(() => { const href = useMemo(() => {
const urip = new AtUri(post.uri) const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey) return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author]) }, [post.uri, post.author])
const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did
const replyAuthorDid = useMemo(() => { const replyAuthorDid = useMemo(() => {
if (!record?.reply) { if (!record?.reply) {
@@ -284,7 +289,14 @@ let FeedItemInner = ({
postEmbed={post.embed} postEmbed={post.embed}
postAuthor={post.author} postAuthor={post.author}
/> />
<PostCtrls post={post} record={record} onPressReply={onPressReply} /> <PostCtrls
post={post}
record={record}
onPressReply={onPressReply}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
/>
</View> </View>
</View> </View>
</Link> </Link>
@@ -364,6 +376,7 @@ const styles = StyleSheet.create({
borderTopWidth: 1, borderTopWidth: 1,
paddingLeft: 10, paddingLeft: 10,
paddingRight: 15, paddingRight: 15,
// @ts-ignore web only -prf
cursor: 'pointer', cursor: 'pointer',
overflow: 'hidden', overflow: 'hidden',
}, },
+7 -1
View File
@@ -27,6 +27,7 @@ import {useSession} from '#/state/session'
export function ProfileCard({ export function ProfileCard({
testID, testID,
profile: profileUnshadowed, profile: profileUnshadowed,
noModFilter,
noBg, noBg,
noBorder, noBorder,
followers, followers,
@@ -35,6 +36,7 @@ export function ProfileCard({
}: { }: {
testID?: string testID?: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
noModFilter?: boolean
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined followers?: AppBskyActorDefs.ProfileView[] | undefined
@@ -50,7 +52,11 @@ export function ProfileCard({
return null return null
} }
const moderation = moderateProfile(profile, moderationOpts) const moderation = moderateProfile(profile, moderationOpts)
if (moderation.account.filter) { if (
!noModFilter &&
moderation.account.filter &&
moderation.account.cause?.type !== 'muted'
) {
return null return null
} }
+1 -9
View File
@@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers' import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger' import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function ProfileFollowers({name}: {name: string}) { export function ProfileFollowers({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
data: resolvedDid, data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollowers({name}: {name: string}) {
<List <List
data={followers} data={followers}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+1 -9
View File
@@ -1,18 +1,16 @@
import React from 'react' import React from 'react'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native' import {ActivityIndicator, StyleSheet, View} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {CenteredView} from '../util/Views' import {CenteredView} from '../util/Views'
import {List} from '../util/List' import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard' import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows' import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri' import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger' import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
export function ProfileFollows({name}: {name: string}) { export function ProfileFollows({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
data: resolvedDid, data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollows({name}: {name: string}) {
<List <List
data={follows} data={follows}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
+30
View File
@@ -0,0 +1,30 @@
import React from 'react'
import {StyleSheet, View, ViewProps} from 'react-native'
import {addStyle} from 'lib/styles'
type BlurViewProps = ViewProps & {
blurType?: 'dark' | 'light'
blurAmount?: number
}
export const BlurView = ({
style,
blurType,
...props
}: React.PropsWithChildren<BlurViewProps>) => {
if (blurType === 'dark') {
style = addStyle(style, styles.dark)
} else {
style = addStyle(style, styles.light)
}
return <View style={style} {...props} />
}
const styles = StyleSheet.create({
dark: {
backgroundColor: '#0008',
},
light: {
backgroundColor: '#fff8',
},
})
+4
View File
@@ -30,6 +30,7 @@ export function H1({children}: React.PropsWithChildren<{}>) {
const styles = useStyles() const styles = useStyles()
const pal = usePalette('default') const pal = usePalette('default')
const typography = useTheme().typography['title-xl'] const typography = useTheme().typography['title-xl']
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
return <ExpoH1 style={[typography, pal.text, styles.h1]}>{children}</ExpoH1> return <ExpoH1 style={[typography, pal.text, styles.h1]}>{children}</ExpoH1>
} }
@@ -37,6 +38,7 @@ export function H2({children}: React.PropsWithChildren<{}>) {
const styles = useStyles() const styles = useStyles()
const pal = usePalette('default') const pal = usePalette('default')
const typography = useTheme().typography['title-lg'] const typography = useTheme().typography['title-lg']
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
return <ExpoH2 style={[typography, pal.text, styles.h2]}>{children}</ExpoH2> return <ExpoH2 style={[typography, pal.text, styles.h2]}>{children}</ExpoH2>
} }
@@ -44,6 +46,7 @@ export function H3({children}: React.PropsWithChildren<{}>) {
const styles = useStyles() const styles = useStyles()
const pal = usePalette('default') const pal = usePalette('default')
const typography = useTheme().typography.title const typography = useTheme().typography.title
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
return <ExpoH3 style={[typography, pal.text, styles.h3]}>{children}</ExpoH3> return <ExpoH3 style={[typography, pal.text, styles.h3]}>{children}</ExpoH3>
} }
@@ -51,6 +54,7 @@ export function H4({children}: React.PropsWithChildren<{}>) {
const styles = useStyles() const styles = useStyles()
const pal = usePalette('default') const pal = usePalette('default')
const typography = useTheme().typography['title-sm'] const typography = useTheme().typography['title-sm']
// @ts-ignore Expo's TextStyle definition seems to have gotten away from RN's -prf
return <ExpoH4 style={[typography, pal.text, styles.h4]}>{children}</ExpoH4> return <ExpoH4 style={[typography, pal.text, styles.h4]}>{children}</ExpoH4>
} }
+43 -3
View File
@@ -1,27 +1,42 @@
import React, {memo, startTransition} from 'react' import React, {memo, startTransition} from 'react'
import {FlatListProps} from 'react-native' import {FlatListProps, RefreshControl} from 'react-native'
import {FlatList_INTERNAL} from './Views' import {FlatList_INTERNAL} from './Views'
import {addStyle} from 'lib/styles'
import {useScrollHandlers} from '#/lib/ScrollContext' import {useScrollHandlers} from '#/lib/ScrollContext'
import {runOnJS, useSharedValue} from 'react-native-reanimated' import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {usePalette} from '#/lib/hooks/usePalette'
export type ListMethods = FlatList_INTERNAL export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit< export type ListProps<ItemT> = Omit<
FlatListProps<ItemT>, FlatListProps<ItemT>,
'onScroll' // Use ScrollContext instead. | 'onScroll' // Use ScrollContext instead.
| 'refreshControl' // Pass refreshing and/or onRefresh instead.
| 'contentOffset' // Pass headerOffset instead.
> & { > & {
onScrolledDownChange?: (isScrolledDown: boolean) => void onScrolledDownChange?: (isScrolledDown: boolean) => void
headerOffset?: number
refreshing?: boolean
onRefresh?: () => void
} }
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null> export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200 const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>( function ListImpl<ItemT>(
{onScrolledDownChange, ...props}: ListProps<ItemT>, {
onScrolledDownChange,
refreshing,
onRefresh,
headerOffset,
style,
...props
}: ListProps<ItemT>,
ref: React.Ref<ListMethods>, ref: React.Ref<ListMethods>,
) { ) {
const isScrolledDown = useSharedValue(false) const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers() const contextScrollHandlers = useScrollHandlers()
const pal = usePalette('default')
function handleScrolledDownChange(didScrollDown: boolean) { function handleScrolledDownChange(didScrollDown: boolean) {
startTransition(() => { startTransition(() => {
@@ -49,11 +64,36 @@ function ListImpl<ItemT>(
}, },
}) })
let refreshControl
if (refreshing !== undefined || onRefresh !== undefined) {
refreshControl = (
<RefreshControl
refreshing={refreshing ?? false}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
)
}
let contentOffset
if (headerOffset != null) {
style = addStyle(style, {
paddingTop: headerOffset,
})
contentOffset = {x: 0, y: headerOffset * -1}
}
return ( return (
<FlatList_INTERNAL <FlatList_INTERNAL
{...props} {...props}
scrollIndicatorInsets={{right: 1}}
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler} onScroll={scrollHandler}
scrollEventThrottle={1} scrollEventThrottle={1}
style={style}
ref={ref} ref={ref}
/> />
) )
+3 -1
View File
@@ -11,6 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {isAndroid} from 'platform/detection' import {isAndroid} from 'platform/detection'
import {TimeElapsed} from './TimeElapsed' import {TimeElapsed} from './TimeElapsed'
import {makeProfileLink} from 'lib/routes/links' import {makeProfileLink} from 'lib/routes/links'
import {ModerationUI} from '@atproto/api'
interface PostMetaOpts { interface PostMetaOpts {
author: { author: {
@@ -23,6 +24,7 @@ interface PostMetaOpts {
postHref: string postHref: string
timestamp: string timestamp: string
showAvatar?: boolean showAvatar?: boolean
avatarModeration?: ModerationUI
avatarSize?: number avatarSize?: number
displayNameType?: TypographyVariant displayNameType?: TypographyVariant
displayNameStyle?: StyleProp<TextStyle> displayNameStyle?: StyleProp<TextStyle>
@@ -41,7 +43,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
<UserAvatar <UserAvatar
avatar={opts.author.avatar} avatar={opts.author.avatar}
size={opts.avatarSize || 16} size={opts.avatarSize || 16}
// TODO moderation moderation={opts.avatarModeration}
/> />
</View> </View>
)} )}

Some files were not shown because too many files have changed in this diff Show More