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 {
getYoutubeVideoId,
makeRecordUri,
toNiceDomain,
toShortUrl,
@@ -12,6 +11,7 @@ import {detectLinkables} from '../../src/lib/strings/rich-text-detection'
import {shortenLinks} from '../../src/lib/strings/rich-text-manip'
import {makeValidHandle, createFullHandle} from '../../src/lib/strings/handles'
import {cleanError} from '../../src/lib/strings/errors'
import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
describe('detectLinkables', () => {
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', () => {
const inputs = [
'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', () => {
for (let i = 0; i < inputs.length; 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.
*/
const IOS_BUILD_NUMBER = '4'
const IOS_BUILD_NUMBER = '2'
/**
* 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
@@ -110,6 +110,9 @@ module.exports = function () {
[
'expo-build-properties',
{
ios: {
deploymentTarget: '13.4',
},
android: {
compileSdkVersion: 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',
state: './src/state',
view: './src/view',
crypto: './src/platform/crypto.ts',
},
},
],
+47 -10
View File
@@ -1,8 +1,11 @@
package main
import (
"encoding/xml"
"fmt"
"net/http"
"strings"
"time"
appbsky "github.com/bluesky-social/indigo/api/bsky"
"github.com/bluesky-social/indigo/atproto/syntax"
@@ -10,6 +13,12 @@ import (
"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".
// Some background: https://book.micro.blog/rss-for-microblogs/
type Item struct {
@@ -17,8 +26,7 @@ type Item struct {
Link string `xml:"link,omitempty"`
Description string `xml:"description,omitempty"`
PubDate string `xml:"pubDate,omitempty"`
Author string `xml:"author,omitempty"`
GUID string `xml:"guid,omitempty"`
GUID ItemGUID
}
type rss struct {
@@ -32,11 +40,33 @@ type rss struct {
func (srv *Server) WebProfileRSS(c echo.Context) error {
ctx := c.Request().Context()
req := c.Request()
didParam := c.Param("did")
did, err := syntax.ParseDID(didParam)
identParam := c.Param("ident")
// 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 {
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", didParam))
return echo.NewHTTPError(400, fmt.Sprintf("not a valid DID: %s", identParam))
}
// check that public view is Ok
@@ -71,12 +101,19 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
if rec.Reply != nil {
continue
}
pubDate := ""
createdAt, err := syntax.ParseDatetimeLenient(rec.CreatedAt)
if nil == err {
pubDate = createdAt.Time().Format(time.RFC822Z)
}
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,
PubDate: rec.CreatedAt,
Author: "@" + pv.Handle,
GUID: aturi.String(),
PubDate: pubDate,
GUID: ItemGUID{
Value: aturi.String(),
IsPerma: false,
},
})
}
@@ -91,7 +128,7 @@ func (srv *Server) WebProfileRSS(c echo.Context) error {
feed := &rss{
Version: "2.0",
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,
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)
// 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
e.GET("/profile/:handle/post/:rkey", server.WebPost)
@@ -336,7 +336,11 @@ func (srv *Server) WebPost(c echo.Context) error {
data["postView"] = postView
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
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)
}
@@ -370,6 +374,7 @@ func (srv *Server) WebProfile(c echo.Context) error {
req := c.Request()
data["profileView"] = pv
data["requestURI"] = fmt.Sprintf("https://%s%s", req.Host, req.URL.Path)
data["requestHost"] = req.Host
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 property="og:description" content="{{ postView.Record.Val.Text }}">
{% endif -%}
{%- if imgThumbUrl %}
{%- if imgThumbUrls %}
{% for imgThumbUrl in imgThumbUrls %}
<meta property="og:image" content="{{ imgThumbUrl }}">
{% endfor %}
<meta name="twitter:card" content="summary_large_image">
{%- elif postView.Author.Avatar %}
{# Don't use avatar image in cards; usually looks bad #}
+3 -1
View File
@@ -34,7 +34,9 @@
{% endif %}
<meta name="twitter:label1" content="Account 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 -%}
{%- endblock %}
+2 -2
View File
@@ -32,7 +32,7 @@ import { Text } from "react-native";
const text = "Hello World";
<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
import { msg } from "@lingui/macro";
import { useLingui } from "@lingui/react";
@@ -116,4 +116,4 @@ export function Welcome() {
### Credits
Please check each individual `messages.po` file for the credits of the translators. We are very grateful for their help!
If you would like to translate the Bluesky app into your language, please open a PR or issue on this repo.
If you would like to translate the Bluesky app into your language, please open a PR or issue on this repo.
+1 -1
View File
@@ -1,6 +1,6 @@
/** @type {import('@lingui/conf').LinguiConfig} */
module.exports = {
locales: ['en', 'hi', 'ja'],
locales: ['en', 'hi', 'ja', 'fr', 'de', 'es'],
catalogs: [
{
path: '<rootDir>/src/locale/locales/{locale}/messages',
+50 -38
View File
@@ -1,10 +1,13 @@
{
"name": "bsky.app",
"version": "1.60.0",
"version": "1.62.0",
"private": true,
"engines": {
"node": ">=18"
},
"scripts": {
"prepare": "is-ci || husky install",
"postinstall": "patch-package",
"postinstall": "patch-package && yarn intl:compile",
"prebuild": "expo prebuild --clean",
"android": "expo run:android",
"ios": "expo run:ios",
@@ -32,7 +35,8 @@
"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:extract": "lingui extract",
"intl:compile": "lingui compile"
"intl:compile": "lingui compile",
"nuke": "rm -rf ./node_modules && rm -rf ./ios && rm -rf ./android"
},
"dependencies": {
"@atproto/api": "^0.7.4",
@@ -49,14 +53,14 @@
"@lingui/react": "^4.5.0",
"@mattermost/react-native-paste-input": "^0.6.4",
"@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-clipboard/clipboard": "^1.10.0",
"@react-native-community/blur": "^4.3.0",
"@react-native-community/datetimepicker": "7.2.0",
"@react-native-masked-view/masked-view": "^0.3.1",
"@react-native-community/datetimepicker": "7.6.1",
"@react-native-masked-view/masked-view": "0.3.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/drawer": "^6.6.2",
"@react-navigation/native": "^6.1.6",
@@ -65,7 +69,7 @@
"@segment/analytics-react": "^1.0.0-rc1",
"@segment/analytics-react-native": "^2.10.1",
"@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",
"@tiptap/core": "^2.0.0-beta.220",
"@tiptap/extension-document": "^2.0.0-beta.220",
@@ -90,24 +94,25 @@
"email-validator": "^2.0.4",
"emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1",
"expo": "^49.0.8",
"expo-application": "~5.3.0",
"expo-build-properties": "~0.8.3",
"expo-camera": "13.5.1",
"expo-constants": "~14.4.2",
"expo-dev-client": "2.4.7",
"expo-device": "~5.4.0",
"expo-image": "~1.3.2",
"expo-image-manipulator": "~11.5.0",
"expo-image-picker": "~14.5.0",
"expo-localization": "~14.3.0",
"expo-media-library": "~15.4.1",
"expo-notifications": "~0.20.1",
"expo-sharing": "~11.5.0",
"expo-splash-screen": "~0.20.5",
"expo-status-bar": "~1.6.0",
"expo-system-ui": "~2.4.0",
"expo-updates": "~0.18.12",
"expo": "^50.0.0-preview.7",
"expo-application": "~5.8.1",
"expo-build-properties": "^0.11.0",
"expo-camera": "~14.0.1",
"expo-constants": "~15.4.2",
"expo-dev-client": "~3.3.4",
"expo-device": "~5.9.1",
"expo-image": "~1.10.1",
"expo-image-manipulator": "^11.8.0",
"expo-image-picker": "~14.7.1",
"expo-localization": "~14.8.1",
"expo-media-library": "~15.9.1",
"expo-notifications": "~0.27.2",
"expo-sharing": "^11.10.0",
"expo-splash-screen": "~0.26.1",
"expo-status-bar": "~1.11.1",
"expo-system-ui": "~2.9.2",
"expo-task-manager": "~11.7.0",
"expo-updates": "~0.24.5",
"fast-text-encoding": "^1.0.6",
"history": "^5.3.0",
"js-sha256": "^0.9.0",
@@ -135,31 +140,34 @@
"react-avatar-editor": "^13.0.0",
"react-circular-progressbar": "^2.1.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-drawer-layout": "^3.2.0",
"react-native-drawer-layout": "^4.0.0-alpha.3",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "^2.12.1",
"react-native-get-random-values": "^1.8.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-get-random-values": "~1.8.0",
"react-native-haptic-feedback": "^1.14.0",
"react-native-image-crop-picker": "^0.38.1",
"react-native-inappbrowser-reborn": "^3.6.3",
"react-native-ios-context-menu": "^1.15.3",
"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-progress": "bluesky-social/react-native-progress",
"react-native-reanimated": "^3.6.0",
"react-native-root-siblings": "^4.1.1",
"react-native-safe-area-context": "4.6.3",
"react-native-screens": "~3.22.0",
"react-native-safe-area-context": "4.7.4",
"react-native-screens": "~3.27.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-uuid": "^2.0.1",
"react-native-version-number": "^0.3.6",
"react-native-web": "~0.19.6",
"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",
"rn-fetch-blob": "^0.12.0",
"sentry-expo": "~7.0.1",
@@ -175,10 +183,13 @@
"@babel/preset-env": "^7.20.0",
"@babel/runtime": "^7.20.0",
"@did-plc/server": "^0.0.1",
"@expo/config-plugins": "7.8.0",
"@expo/prebuild-config": "6.7.0",
"@lingui/cli": "^4.5.0",
"@lingui/macro": "^4.5.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.11",
"@react-native-community/eslint-config": "^3.0.0",
"@react-native/typescript-config": "^0.74.0",
"@testing-library/jest-native": "^5.4.1",
"@testing-library/react-native": "^11.5.2",
"@tsconfig/react-native": "^2.0.3",
@@ -199,11 +210,12 @@
"@types/react-test-renderer": "^17.0.1",
"@typescript-eslint/eslint-plugin": "^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-plugin-macros": "^3.1.0",
"babel-plugin-module-resolver": "^5.0.0",
"babel-plugin-react-native-web": "^0.18.12",
"babel-preset-expo": "^10.0.0",
"detox": "^20.13.0",
"eslint": "^8.19.0",
"eslint-plugin-detox": "^1.0.0",
@@ -214,8 +226,8 @@
"html-webpack-plugin": "^5.5.0",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"jest": "^29.4.3",
"jest-expo": "^49.0.0",
"jest": "^29.7.0",
"jest-expo": "^50.0.1",
"jest-junit": "^15.0.0",
"lint-staged": "^13.2.3",
"metro-react-native-babel-preset": "^0.73.7",
@@ -225,7 +237,7 @@
"react-scripts": "^5.0.1",
"react-test-renderer": "18.2.0",
"ts-node": "^10.9.1",
"typescript": "^5.1.3",
"typescript": "^5.3.3",
"url-loader": "^4.1.1",
"webpack": "^5.75.0",
"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
index 27d4cb3..fd71f47 100644
index cae11e7..42f251b 100644
--- a/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 dependencies;
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
// 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) {
wrappedAst = ast;
} 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
--- a/node_modules/react-native/Libraries/Text/TextInput/RCTBackedTextInputDelegateAdapter.m
+++ b/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.mm
@@ -266,11 +266,10 @@ static void *TextFieldSelectionObservingContext = &TextFieldSelectionObservingCo
- (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 {State, RouteParams} from 'lib/routes/types'
import {colors} from 'lib/styles'
import {isNative} from 'platform/detection'
import {isAndroid, isNative} from 'platform/detection'
import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle'
import {router} from './routes'
import {usePalette} from 'lib/hooks/usePalette'
@@ -286,6 +286,7 @@ function HomeTabNavigator() {
return (
<HomeTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
@@ -307,6 +308,7 @@ function SearchTabNavigator() {
return (
<SearchTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
@@ -324,6 +326,7 @@ function FeedsTabNavigator() {
return (
<FeedsTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
@@ -345,6 +348,7 @@ function NotificationsTabNavigator() {
return (
<NotificationsTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
@@ -366,6 +370,7 @@ function MyProfileTabNavigator() {
return (
<MyProfileTab.Navigator
screenOptions={{
animation: isAndroid ? 'none' : undefined,
gestureEnabled: true,
fullScreenGestureEnabled: true,
headerShown: false,
+20 -15
View File
@@ -40,8 +40,6 @@ type Props = {
isReady: boolean
}
SplashScreen.preventAutoHideAsync().catch(() => {})
const AnimatedLogo = Animated.createAnimatedComponent(Logo)
export function Splash(props: React.PropsWithChildren<Props>) {
@@ -49,6 +47,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
const intro = useSharedValue(0)
const outroLogo = useSharedValue(0)
const outroApp = useSharedValue(0)
const outroAppOpacity = useSharedValue(0)
const [isAnimationComplete, setIsAnimationComplete] = React.useState(false)
const [isImageLoaded, setIsImageLoaded] = React.useState(false)
const isReady = props.isReady && isImageLoaded
@@ -62,8 +61,8 @@ export function Splash(props: React.PropsWithChildren<Props>) {
{
scale: interpolate(
outroLogo.value,
[0, 0.06, 0.08, 1],
[1, 0.8, 0.8, 400],
[0, 0.08, 1],
[1, 0.8, 400],
'clamp',
),
},
@@ -79,7 +78,12 @@ export function Splash(props: React.PropsWithChildren<Props>) {
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(
1,
{duration: 200, easing: Easing.out(Easing.cubic)},
{duration: 400, easing: Easing.out(Easing.cubic)},
async () => {
// set these values to check animation at specific point
// outroLogo.value = 0.1
// outroApp.value = 0.1
outroLogo.value = withTiming(
1,
{duration: 1000, easing: Easing.in(Easing.cubic)},
() => {
runOnJS(onFinish)()
},
)
outroApp.value = withTiming(
1,
{duration: 1000, easing: Easing.inOut(Easing.cubic)},
{duration: 1200, easing: Easing.in(Easing.cubic)},
() => {
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(() => {
setIsImageLoaded(true)
+3 -2
View File
@@ -1,9 +1,10 @@
import {useColorScheme} from 'react-native'
import * as persisted from '#/state/persisted'
import {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
export function useColorModeTheme(
theme: persisted.Schema['colorMode'],
): 'light' | 'dark' {
const colorScheme = useColorScheme_FIXED()
const colorScheme = useColorScheme()
return (theme === 'system' ? colorScheme : theme) || 'light'
}
+7 -3
View File
@@ -1,7 +1,11 @@
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 {useColorScheme_FIXED} from '#/lib/hooks/useColorScheme_FIXED'
export type ColorScheme = 'light' | 'dark'
@@ -95,7 +99,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({
theme,
children,
}) => {
const colorScheme = useColorScheme_FIXED()
const colorScheme = useColorScheme()
const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
return (
-1
View File
@@ -13,7 +13,6 @@ interface TrackPropertiesMap {
'Sign In': {resumedSession: boolean} // CAN BE SERVER
'Create Account': {} // CAN BE SERVER
'Try Create Account': {}
'Create Account Successfully': {}
'Signin:PressedForgotPassword': {}
'Signin:PressedSelectService': {}
// COMPOSER / CREATE POST events
+10 -9
View File
@@ -117,11 +117,7 @@ export class FeedViewPostsSlice {
}
export class NoopFeedTuner {
private keyCounter = 0
reset() {
this.keyCounter = 0
}
reset() {}
tune(
feed: FeedViewPost[],
_opts?: {dryRun: boolean; maintainOrder: boolean},
@@ -131,13 +127,13 @@ export class NoopFeedTuner {
}
export class FeedTuner {
private keyCounter = 0
seenKeys: Set<string> = new Set()
seenUris: Set<string> = new Set()
constructor(public tunerFns: FeedTunerFn[]) {}
reset() {
this.keyCounter = 0
this.seenKeys.clear()
this.seenUris.clear()
}
@@ -218,11 +214,16 @@ export class FeedTuner {
}
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) {
this.seenUris.add(item.post.uri)
}
}
this.seenKeys.add(slice._reactKey)
return true
})
}
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
}
+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.
*
+2 -2
View File
@@ -7,6 +7,6 @@ test('sanitizeAppLanguageSetting', () => {
expect(sanitizeAppLanguageSetting('en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('hi')).toBe(AppLanguage.hi)
expect(sanitizeAppLanguageSetting('foo')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,fr')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('fr,en')).toBe(AppLanguage.en)
expect(sanitizeAppLanguageSetting('en,foo')).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
case '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:
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 messagesHi} from '#/locale/locales/hi/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 {AppLanguage} from '#/locale/languages'
@@ -21,6 +27,20 @@ export async function dynamicActivate(locale: AppLanguage) {
i18n.loadAndActivate({locale, messages: messagesJa})
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: {
i18n.loadAndActivate({locale, messages: messagesEn})
break
+14
View File
@@ -20,6 +20,20 @@ export async function dynamicActivate(locale: AppLanguage) {
mod = await import(`./locales/ja/messages`)
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: {
mod = await import(`./locales/en/messages`)
break
+10
View File
@@ -8,6 +8,11 @@ export enum AppLanguage {
en = 'en',
hi = 'hi',
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 {
@@ -19,6 +24,11 @@ export const APP_LANGUAGES: AppLanguageConfig[] = [
{code2: AppLanguage.en, name: 'English'},
{code2: AppLanguage.hi, 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[] = [
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 = {},
) {
if (!this.enabled) return
if (!enabledLogLevels[this.level].includes(level)) return
const timestamp = Date.now()
const meta = metadata || {}
for (const transport of this.transports) {
transport(level, message, meta, timestamp)
}
// send every log to syslog
add({
id: nanoid(),
timestamp,
@@ -303,6 +299,12 @@ export class Logger {
message,
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) {
entries.unshift(entry)
entries = entries.slice(0, 50)
entries = entries.slice(0, 500)
}
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
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: {
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({
step: z.string(),
}),
hiddenPosts: z.array(z.string()).optional(), // should move to server
})
export type Schema = z.infer<typeof schema>
@@ -66,4 +67,5 @@ export const defaults: Schema = {
onboarding: {
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 {Provider as LanguagesProvider} from './languages'
import {Provider as AltTextRequiredProvider} from '../preferences/alt-text-required'
import {Provider as HiddenPostsProvider} from '../preferences/hidden-posts'
export {useLanguagePrefs, useLanguagePrefsApi} from './languages'
export {
useRequireAltTextEnabled,
useSetRequireAltTextEnabled,
} from './alt-text-required'
export * from './hidden-posts'
export function Provider({children}: React.PropsWithChildren<{}>) {
return (
<LanguagesProvider>
<AltTextRequiredProvider>{children}</AltTextRequiredProvider>
<AltTextRequiredProvider>
<HiddenPostsProvider>{children}</HiddenPostsProvider>
</AltTextRequiredProvider>
</LanguagesProvider>
)
}
+3 -2
View File
@@ -11,6 +11,7 @@ import {
getModerationOpts,
useModerationOpts,
} from './preferences'
import {isInvalidHandle} from '#/lib/strings/handles'
const DEFAULT_MOD_OPTS = getModerationOpts({
userDid: '',
@@ -111,7 +112,7 @@ function computeSuggestions(
}
return items.filter(profile => {
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,
info: AppBskyActorDefs.ProfileViewBasic,
): boolean {
if (info.handle.includes(prefix)) {
if (!isInvalidHandle(info.handle) && info.handle.includes(prefix)) {
return true
}
if (info.displayName?.toLocaleLowerCase().includes(prefix)) {
+5 -1
View File
@@ -218,11 +218,13 @@ const FOLLOWING_FEED_STUB: FeedSourceInfo = {
export function usePinnedFeedsInfos(): {
feeds: FeedSourceInfo[]
hasPinnedCustom: boolean
isLoading: boolean
} {
const queryClient = useQueryClient()
const [tabs, setTabs] = React.useState<FeedSourceInfo[]>([
FOLLOWING_FEED_STUB,
])
const [isLoading, setLoading] = React.useState(true)
const {data: preferences} = usePreferencesQuery()
const hasPinnedCustom = React.useMemo<boolean>(() => {
@@ -249,6 +251,7 @@ export function usePinnedFeedsInfos(): {
// these requests can fail, need to filter those out
try {
return await queryClient.fetchQuery({
staleTime: STALE.SECONDS.FIFTEEN,
queryKey: feedSourceInfoQueryKey({uri}),
queryFn: async () => {
const type = getFeedTypeFromUri(uri)
@@ -283,10 +286,11 @@ export function usePinnedFeedsInfos(): {
) as FeedSourceInfo[]
setTabs([FOLLOWING_FEED_STUB].concat(views))
setLoading(false)
}
fetchFeedInfo()
}, [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
setNumUnread('')
broadcast.postMessage({event: ''})
if (isNative) {
Notifications.setBadgeCountAsync(0)
}
},
async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
+4 -2
View File
@@ -2,12 +2,12 @@ import {
AppBskyNotificationListNotifications,
ModerationOpts,
moderateProfile,
moderatePost,
AppBskyFeedDefs,
AppBskyFeedPost,
AppBskyFeedRepost,
AppBskyFeedLike,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import chunk from 'lodash.chunk'
import {QueryClient} from '@tanstack/react-query'
import {getAgent} from '../../session'
@@ -156,7 +156,7 @@ async function fetchSubjects(
): Promise<Map<string, AppBskyFeedDefs.PostView>> {
const uris = new Set<string>()
for (const notif of groupedNotifs) {
if (notif.subjectUri) {
if (notif.subjectUri && !notif.subjectUri.includes('feed.generator')) {
uris.add(notif.subjectUri)
}
}
@@ -216,6 +216,8 @@ function getSubjectUri(
? notif.record.subject?.uri
: 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 {
AppBskyFeedDefs,
AppBskyFeedPost,
moderatePost,
PostModeration,
} from '@atproto/api'
import {AppBskyFeedDefs, AppBskyFeedPost, PostModeration} from '@atproto/api'
import {
useInfiniteQuery,
InfiniteData,
@@ -12,6 +7,7 @@ import {
QueryClient,
useQueryClient,
} from '@tanstack/react-query'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {useFeedTuners} from '../preferences/feed-tuners'
import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
import {FeedAPI, ReasonFeedSource} from 'lib/api/feed/types'
+9 -2
View File
@@ -19,6 +19,7 @@ import {
} from '#/state/queries/preferences/const'
import {getModerationOpts} from '#/state/queries/preferences/moderation'
import {STALE} from '#/state/queries'
import {useHiddenPosts} from '#/state/preferences/hidden-posts'
export * from '#/state/queries/preferences/types'
export * from '#/state/queries/preferences/moderation'
@@ -94,15 +95,21 @@ export function usePreferencesQuery() {
export function useModerationOpts() {
const {currentAccount} = useSession()
const prefs = usePreferencesQuery()
const hiddenPosts = useHiddenPosts()
const opts = useMemo(() => {
if (!prefs.data) {
return
}
return getModerationOpts({
const moderationOpts = getModerationOpts({
userDid: currentAccount?.did || '',
preferences: prefs.data,
})
}, [currentAccount?.did, prefs.data])
return {
...moderationOpts,
hiddenPosts,
}
}, [currentAccount?.did, prefs.data, hiddenPosts])
return opts
}
+2
View File
@@ -189,6 +189,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
},
logger.DebugContext.session,
)
track('Try Create Account')
const agent = new BskyAgent({service})
@@ -231,6 +232,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
},
logger.DebugContext.session,
)
track('Create Account')
},
[upsertAccount, queryClient],
)
+44 -3
View File
@@ -3,8 +3,9 @@ import {View, Pressable} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {useLingui} from '@lingui/react'
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 {CreateAccount} from 'view/com/auth/create/CreateAccount'
import {ErrorBoundary} from 'view/com/util/ErrorBoundary'
@@ -18,6 +19,9 @@ import {
useLoggedOutView,
useLoggedOutViewControls,
} 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 {
S_LoginOrCreateAccount,
@@ -26,6 +30,7 @@ enum ScreenState {
}
export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
const {hasSession} = useSession()
const {_} = useLingui()
const pal = usePalette('default')
const setMinimalShellMode = useSetMinimalShellMode()
@@ -40,6 +45,8 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
)
const {isMobile} = useWebMediaQueries()
const {clearRequestedAccount} = useLoggedOutViewControls()
const navigation = useNavigation<NavigationProp>()
const isFirstScreen = screenState === ScreenState.S_LoginOrCreateAccount
React.useEffect(() => {
screen('Login')
@@ -53,6 +60,10 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
clearRequestedAccount()
}, [clearRequestedAccount, onDismiss])
const onPressSearch = React.useCallback(() => {
navigation.navigate(`SearchTab`)
}, [navigation])
return (
<View
testID="noSessionView"
@@ -65,7 +76,7 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
},
]}>
<ErrorBoundary>
{onDismiss && (
{onDismiss ? (
<Pressable
accessibilityHint={_(msg`Go back`)}
accessibilityLabel={_(msg`Go back`)}
@@ -88,7 +99,37 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}}
/>
</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 ? (
<SplashScreen
+55 -60
View File
@@ -1,7 +1,6 @@
import React from 'react'
import {
ActivityIndicator,
KeyboardAvoidingView,
ScrollView,
StyleSheet,
TouchableOpacity,
@@ -28,9 +27,10 @@ import {IS_PROD} from '#/lib/constants'
import {Step1} from './Step1'
import {Step2} from './Step2'
import {Step3} from './Step3'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {track, screen} = useAnalytics()
const {screen} = useAnalytics()
const pal = usePalette('default')
const {_} = useLingui()
const [uiState, uiDispatch] = useCreateAccount()
@@ -38,6 +38,7 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
const {createAccount} = useSessionApi()
const {mutate: setBirthDate} = usePreferencesSetBirthDateMutation()
const {mutate: setSavedFeeds} = useSetSaveFeedsMutation()
const {isTabletOrDesktop} = useWebMediaQueries()
React.useEffect(() => {
screen('CreateAccount')
@@ -93,21 +94,17 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
uiDispatch,
_,
})
track('Create Account')
setBirthDate({birthDate: uiState.birthDate})
if (IS_PROD(uiState.serviceUrl)) {
setSavedFeeds(DEFAULT_PROD_FEEDS)
}
} catch {
// dont need to handle here
} finally {
track('Try Create Account')
}
}
}, [
uiState,
uiDispatch,
track,
onboardingDispatch,
createAccount,
setBirthDate,
@@ -124,64 +121,62 @@ export function CreateAccount({onPressBack}: {onPressBack: () => void}) {
title={_(msg`Create Account`)}
description={_(msg`We're so excited to have you join us!`)}>
<ScrollView testID="createAccount" style={pal.view}>
<KeyboardAvoidingView behavior="padding">
<View style={styles.stepContainer}>
{uiState.step === 1 && (
<Step1 uiState={uiState} uiDispatch={uiDispatch} />
)}
{uiState.step === 2 && (
<Step2 uiState={uiState} uiDispatch={uiDispatch} />
)}
{uiState.step === 3 && (
<Step3 uiState={uiState} uiDispatch={uiDispatch} />
)}
</View>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<View style={styles.stepContainer}>
{uiState.step === 1 && (
<Step1 uiState={uiState} uiDispatch={uiDispatch} />
)}
{uiState.step === 2 && (
<Step2 uiState={uiState} uiDispatch={uiDispatch} />
)}
{uiState.step === 3 && (
<Step3 uiState={uiState} uiDispatch={uiDispatch} />
)}
</View>
<View style={[s.flexRow, s.pl20, s.pr20]}>
<TouchableOpacity
onPress={onPressBackInner}
testID="backBtn"
accessibilityRole="button">
<Text type="xl" style={pal.link}>
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{uiState.canNext ? (
<TouchableOpacity
onPress={onPressBackInner}
testID="backBtn"
testID="nextBtn"
onPress={onPressNext}
accessibilityRole="button">
<Text type="xl" style={pal.link}>
<Trans>Back</Trans>
{uiState.isProcessing ? (
<ActivityIndicator />
) : (
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
)}
</TouchableOpacity>
) : serviceInfoError ? (
<TouchableOpacity
testID="retryConnectBtn"
onPress={() => refetchServiceInfo()}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint=""
accessibilityLiveRegion="polite">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
<View style={s.flex1} />
{uiState.canNext ? (
<TouchableOpacity
testID="nextBtn"
onPress={onPressNext}
accessibilityRole="button">
{uiState.isProcessing ? (
<ActivityIndicator />
) : (
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Next</Trans>
</Text>
)}
</TouchableOpacity>
) : serviceInfoError ? (
<TouchableOpacity
testID="retryConnectBtn"
onPress={() => refetchServiceInfo()}
accessibilityRole="button"
accessibilityLabel={_(msg`Retry`)}
accessibilityHint=""
accessibilityLiveRegion="polite">
<Text type="xl-bold" style={[pal.link, s.pr5]}>
<Trans>Retry</Trans>
</Text>
</TouchableOpacity>
) : serviceInfoIsFetching ? (
<>
<ActivityIndicator color="#fff" />
<Text type="xl" style={[pal.text, s.pr5]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : undefined}
</View>
<View style={s.footerSpacer} />
</KeyboardAvoidingView>
) : serviceInfoIsFetching ? (
<>
<ActivityIndicator color="#fff" />
<Text type="xl" style={[pal.text, s.pr5]}>
<Trans>Connecting...</Trans>
</Text>
</>
) : undefined}
</View>
<View style={{height: isTabletOrDesktop ? 50 : 400}} />
</ScrollView>
</LoggedOutLayout>
)
+26 -1
View File
@@ -13,6 +13,17 @@ import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
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
* @field Invite code or waitlist
@@ -38,6 +49,10 @@ export function Step2({
openModal({name: 'waitlist'})
}, [openModal])
const birthDate = React.useMemo(() => {
return sanitizeDate(uiState.birthDate)
}, [uiState.birthDate])
return (
<View>
<StepHeader step="2" title={_(msg`Your account`)} />
@@ -56,6 +71,9 @@ export function Step2({
onChange={value => uiDispatch({type: 'set-invite-code', value})}
accessibilityLabel={_(msg`Invite code`)}
accessibilityHint="Input invite code to proceed"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/>
</View>
)}
@@ -90,6 +108,9 @@ export function Step2({
accessibilityLabel={_(msg`Email`)}
accessibilityHint="Input email for Bluesky waitlist"
accessibilityLabelledBy="email"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/>
</View>
@@ -111,6 +132,9 @@ export function Step2({
accessibilityLabel={_(msg`Password`)}
accessibilityHint="Set password"
accessibilityLabelledBy="password"
autoCapitalize="none"
autoComplete="off"
autoCorrect={false}
/>
</View>
@@ -122,8 +146,9 @@ export function Step2({
<Trans>Your birth date</Trans>
</Text>
<DateInput
handleAsUTC
testID="birthdayInput"
value={uiState.birthDate}
value={birthDate}
onChange={value => uiDispatch({type: 'set-birth-date', value})}
buttonType="default-light"
buttonStyle={[pal.border, styles.dateInputButton]}
+1 -1
View File
@@ -144,7 +144,7 @@ export async function submit({
}
export function is13(state: CreateAccountState) {
return getAge(state.birthDate) >= 18
return getAge(state.birthDate) >= 13
}
export function is18(state: CreateAccountState) {
+1
View File
@@ -174,6 +174,7 @@ export const LoginForm = ({
autoCorrect={false}
autoComplete="username"
returnKeyType="next"
textContentType="username"
onSubmitEditing={() => {
passwordInputRef.current?.focus()
}}
+7 -3
View File
@@ -207,7 +207,11 @@ export const ComposePost = observer(function ComposePost({
setError('')
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
}
@@ -438,7 +442,7 @@ export const ComposePost = observer(function ComposePost({
accessibilityLabel={_(msg`Add link card`)}
accessibilityHint={`Creates a card with a thumbnail. The card links to ${url}`}>
<Text style={pal.text}>
<Trans>Add link card:</Trans>
<Trans>Add link card:</Trans>{' '}
<Text style={[pal.link, s.ml5]}>{toShortUrl(url)}</Text>
</Text>
</TouchableOpacity>
@@ -452,7 +456,7 @@ export const ComposePost = observer(function ComposePost({
<OpenCameraBtn gallery={gallery} />
</>
) : null}
{isDesktop ? <EmojiPickerButton /> : null}
{!isMobile ? <EmojiPickerButton /> : null}
<View style={s.flex1} />
<SelectLangBtn />
<CharProgress count={graphemeLength} />
@@ -215,7 +215,13 @@ export const TextInput = forwardRef(function TextInputImpl(
autoFocus={true}
allowFontScaling
multiline
style={[pal.text, styles.textInput, styles.textInputFormatting]}
numberOfLines={4}
style={[
pal.text,
styles.textInput,
styles.textInputFormatting,
{textAlignVertical: 'top'},
]}
{...props}>
{textDecorated}
</PasteInput>
@@ -134,7 +134,7 @@ const MentionList = forwardRef<MentionListRef, SuggestionProps>(
return true
}
if (event.key === 'Enter') {
if (event.key === 'Enter' || event.key === 'Tab') {
enterHandler()
return true
}
@@ -76,7 +76,7 @@ export function EmojiPicker({close}: {close: () => void}) {
return (await import('./EmojiPickerData.json')).default
}}
onEmojiSelect={onInsert}
autoFocus={false}
autoFocus={true}
/>
</View>
</TouchableWithoutFeedback>
@@ -96,6 +96,7 @@ const styles = StyleSheet.create({
},
trigger: {
backgroundColor: 'transparent',
// @ts-ignore web only -prf
border: 'none',
paddingTop: 4,
paddingLeft: 12,
+4 -19
View File
@@ -1,12 +1,5 @@
import React from 'react'
import {
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {FeedSourceCardLoaded} from './FeedSourceCard'
@@ -180,22 +173,14 @@ export const ProfileFeedgens = React.forwardRef<
data={items}
keyExtractor={(item: any) => item._reactKey || item.uri}
renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
desktopFixedHeight
onEndReached={onEndReached}
@@ -320,6 +320,7 @@ const ImageItem = ({
accessibilityLabel={imageSrc.alt}
accessibilityHint=""
onLoad={() => setIsLoaded(true)}
cachePolicy="memory"
/>
</GestureDetector>
</Animated.View>
+4 -14
View File
@@ -2,7 +2,6 @@ import React from 'react'
import {
ActivityIndicator,
Dimensions,
RefreshControl,
StyleProp,
View,
ViewStyle,
@@ -15,7 +14,6 @@ import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {ProfileCard} from '../profile/ProfileCard'
import {Button} from '../util/forms/Button'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {useListMembersQuery} from '#/state/queries/list-members'
import {logger} from '#/logger'
@@ -51,7 +49,6 @@ export function ListMembers({
headerOffset?: number
desktopFixedHeightOffset?: number
}) {
const pal = usePalette('default')
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries()
@@ -183,6 +180,7 @@ export function ListMembers({
profile={(item as AppBskyGraphDefs.ListItemView).subject}
renderButton={renderMemberButton}
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
noModFilter
/>
)
},
@@ -215,24 +213,16 @@ export function ListMembers({
renderItem={renderItem}
ListHeaderComponent={renderHeader}
ListFooterComponent={Footer}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
refreshing={isRefreshing}
onRefresh={onRefresh}
headerOffset={headerOffset}
contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
desktopFixedHeight={desktopFixedHeightOffset || true}
/>
+45 -25
View File
@@ -119,31 +119,51 @@ export function MyLists({
[error, onRefresh, renderItem, pal],
)
const FlatListCom = inline ? RNFlatList : List
return (
<View testID={testID} style={style}>
{items.length > 0 && (
<FlatListCom
testID={testID ? `${testID}-flatlist` : undefined}
data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
contentContainerStyle={[s.contentContainer]}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
if (inline) {
return (
<View testID={testID} style={style}>
{items.length > 0 && (
<RNFlatList
testID={testID ? `${testID}-flatlist` : undefined}
data={items}
keyExtractor={item => (item.uri ? item.uri : item._reactKey)}
renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
contentContainerStyle={[s.contentContainer]}
removeClippedSubviews={true}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</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({
+4 -19
View File
@@ -1,12 +1,5 @@
import React from 'react'
import {
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {Dimensions, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {useQueryClient} from '@tanstack/react-query'
import {List, ListRef} from '../util/List'
import {ListCard} from './ListCard'
@@ -182,22 +175,14 @@ export const ProfileLists = React.forwardRef<SectionRef, ProfileListsProps>(
data={items}
keyExtractor={(item: any) => item._reactKey}
renderItem={renderItemInner}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
desktopFixedHeight
onEndReached={onEndReached}
+1
View File
@@ -80,6 +80,7 @@ export function Component({image}: Props) {
source={{
uri: image.cropped?.path ?? image.path,
}}
contentFit="contain"
accessible={true}
accessibilityIgnoresInvertColors
/>
+3 -3
View File
@@ -62,17 +62,17 @@ export function Component(props: ReportComponentProps) {
<Text
type="2xl-bold"
style={[pal.text, s.textCenter, {paddingBottom: 8}]}>
<Trans>Appeal Decision</Trans>
<Trans>Appeal Content Warning</Trans>
</Text>
<ScrollView>
<View style={[pal.btn, styles.detailsInputContainer]}>
<TextInput
accessibilityLabel={_(msg`Text input field`)}
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={_(
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}
value={details}
+2 -1
View File
@@ -23,7 +23,7 @@ import {
} from '#/state/queries/preferences'
import {logger} from '#/logger'
export const snapPoints = ['50%']
export const snapPoints = ['50%', '90%']
function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
const pal = usePalette('default')
@@ -63,6 +63,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
<View>
<DateInput
handleAsUTC
testID="birthdayInput"
value={date}
onChange={setDate}
+1 -1
View File
@@ -82,7 +82,7 @@ export function ModalsContainer() {
useEffect(() => {
if (isModalActive) {
bottomSheetRef.current?.expand()
bottomSheetRef.current?.snapToIndex(0)
} else {
bottomSheetRef.current?.close()
}
+2 -2
View File
@@ -69,7 +69,7 @@ export function Component({
<ScrollView>
<Text style={[pal.text, styles.description]}>
Choose "Everybody" or "Nobody"
<Trans>Choose "Everybody" or "Nobody"</Trans>
</Text>
<View style={{flexDirection: 'row', gap: 6, paddingHorizontal: 6}}>
<Selectable
@@ -86,7 +86,7 @@ export function Component({
/>
</View>
<Text style={[pal.text, styles.description]}>
Or combine these options:
<Trans>Or combine these options:</Trans>
</Text>
<View style={{flexDirection: 'column', gap: 4, paddingHorizontal: 6}}>
<Selectable
@@ -42,7 +42,8 @@ export function InputIssueDetails({
accessibilityHint="Add more details to your report">
<FontAwesomeIcon size={18} icon="angle-left" style={[pal.link]} />
<Text style={[pal.text, s.f18, pal.link]}>
<Trans> Back</Trans>
{' '}
<Trans>Back</Trans>
</Text>
</TouchableOpacity>
<View style={[pal.btn, styles.detailsInputContainer]}>
+3 -3
View File
@@ -44,9 +44,9 @@ export function Component(content: ReportComponentProps) {
const {isMobile} = useWebMediaQueries()
const [isProcessing, setIsProcessing] = useState(false)
const [showDetailsInput, setShowDetailsInput] = useState(false)
const [error, setError] = useState<string>()
const [issue, setIssue] = useState<string>()
const [details, setDetails] = useState<string>()
const [error, setError] = useState<string>('')
const [issue, setIssue] = useState<string>('')
const [details, setDetails] = useState<string>('')
const isAccountReport = 'did' in content
const subjectKey = isAccountReport ? content.did : content.uri
const atUri = useMemo(
+3 -11
View File
@@ -1,13 +1,12 @@
import React from 'react'
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 {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {EmptyState} from '../util/EmptyState'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger'
@@ -30,7 +29,6 @@ export function Feed({
onScrolledDownChange: (isScrolledDown: boolean) => void
ListHeaderComponent?: () => JSX.Element
}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false)
const moderationOpts = useModerationOpts()
@@ -152,14 +150,8 @@ export function Feed({
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScrolledDownChange={onScrolledDownChange}
+16 -2
View File
@@ -42,6 +42,7 @@ import {TimeElapsed} from '../util/TimeElapsed'
import {isWeb} from 'platform/detection'
import {Trans, msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {FeedSourceCard} from '../feeds/FeedSourceCard'
const MAX_AUTHORS = 5
@@ -112,7 +113,7 @@ let FeedItem = ({
]
}, [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
return <View />
}
@@ -166,7 +167,7 @@ let FeedItem = ({
iconStyle = [s.blue3 as FontAwesomeIconStyle]
} else if (item.type === 'feedgen-like') {
action = `liked your custom feed${
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}}'` : ''
item.subjectUri ? ` '${new AtUri(item.subjectUri).rkey}'` : ''
}`
icon = 'HeartIconSolid'
iconStyle = [
@@ -256,6 +257,13 @@ let FeedItem = ({
{item.type === 'post-like' || item.type === 'repost' ? (
<AdditionalPostText post={item.subject} />
) : null}
{item.type === 'feedgen-like' && item.subjectUri ? (
<FeedSourceCard
feedUri={item.subjectUri}
style={[pal.view, pal.border, styles.feedcard]}
showLikes
/>
) : null}
</View>
</Link>
)
@@ -496,6 +504,12 @@ const styles = StyleSheet.create({
marginLeft: 2,
opacity: 0.8,
},
feedcard: {
borderWidth: 1,
borderRadius: 8,
paddingVertical: 12,
marginTop: 6,
},
addedContainer: {
paddingTop: 4,
+3 -11
View File
@@ -1,18 +1,16 @@
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 {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostLikedByQuery} from '#/state/queries/post-liked-by'
import {cleanError} from '#/lib/strings/errors'
export function PostLikedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false)
const {
data: resolvedUri,
@@ -88,14 +86,8 @@ export function PostLikedBy({uri}: {uri: string}) {
<List
data={likes}
keyExtractor={item => item.actor.did}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
+3 -11
View File
@@ -1,18 +1,16 @@
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 {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {usePalette} from 'lib/hooks/usePalette'
import {logger} from '#/logger'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {usePostRepostedByQuery} from '#/state/queries/post-reposted-by'
import {cleanError} from '#/lib/strings/errors'
export function PostRepostedBy({uri}: {uri: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = useState(false)
const {
data: resolvedUri,
@@ -89,14 +87,8 @@ export function PostRepostedBy({uri}: {uri: string}) {
<List
data={repostedBy}
keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
+2 -9
View File
@@ -2,7 +2,6 @@ import React, {useEffect, useRef} from 'react'
import {
ActivityIndicator,
Pressable,
RefreshControl,
StyleSheet,
TouchableOpacity,
View,
@@ -349,14 +348,8 @@ function PostThreadLoaded({
}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onPTR}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onPTR}
onContentSizeChange={onContentSizeChange}
style={s.hContentRegion}
// @ts-ignore our .web version only -prf
+7 -11
View File
@@ -5,9 +5,9 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
RichText as RichTextAPI,
moderatePost,
PostModeration,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Link, TextLink} from '../util/Link'
import {RichText} from '../util/text/RichText'
@@ -42,7 +42,6 @@ import {useComposerControls} from '#/state/shell/composer'
import {useModerationOpts} from '#/state/queries/preferences'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {ThreadPost} from '#/state/queries/post-thread'
import {LabelInfo} from '../util/moderation/LabelInfo'
import {useSession} from '#/state/session'
import {WhoCanReply} from '../threadgate/WhoCanReply'
@@ -187,9 +186,9 @@ let PostThreadItemLoaded = ({
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
}, [post.uri, post.author])
const repostsTitle = 'Reposts of this post'
const isSelfLabeledPost =
const isModeratedPost =
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(
record?.text || '',
@@ -335,6 +334,9 @@ let PostThreadItemLoaded = ({
postCid={post.cid}
postUri={post.uri}
record={record}
showAppealLabelItem={
post.author.did === currentAccount?.did && isModeratedPost
}
style={{
paddingVertical: 6,
paddingHorizontal: 10,
@@ -354,13 +356,6 @@ let PostThreadItemLoaded = ({
includeMute
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 ? (
<View
style={[
@@ -544,6 +539,7 @@ let PostThreadItemLoaded = ({
timestamp={post.indexedAt}
postHref={postHref}
showAvatar={isThreadedChild}
avatarModeration={moderation.avatar}
avatarSize={28}
displayNameType="md-bold"
displayNameStyle={isThreadedChild && s.ml2}
+2 -1
View File
@@ -4,10 +4,10 @@ import {
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
moderatePost,
PostModeration,
RichText as RichTextAPI,
} from '@atproto/api'
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {Link, TextLink} from '../util/Link'
import {UserInfoText} from '../util/UserInfoText'
@@ -221,6 +221,7 @@ const styles = StyleSheet.create({
paddingBottom: 5,
paddingLeft: 10,
borderTopWidth: 1,
// @ts-ignore web only -prf
cursor: 'pointer',
},
layout: {
+7 -15
View File
@@ -3,7 +3,6 @@ import {
ActivityIndicator,
AppState,
Dimensions,
RefreshControl,
StyleProp,
StyleSheet,
View,
@@ -16,7 +15,6 @@ import {FeedErrorMessage} from './FeedErrorMessage'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {useAnalytics} from 'lib/analytics/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
@@ -74,7 +72,6 @@ let Feed = ({
ListHeaderComponent?: () => JSX.Element
extraData?: any
}): React.ReactNode => {
const pal = usePalette('default')
const theme = useTheme()
const {track} = useAnalytics()
const queryClient = useQueryClient()
@@ -98,10 +95,13 @@ let Feed = ({
isFetchingNextPage,
fetchNextPage,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
if (data?.pages[0]) {
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 () => {
if (!data?.pages[0] || isFetching || !onHasNew || !enabled) {
@@ -294,25 +294,17 @@ let Feed = ({
renderItem={renderItem}
ListFooterComponent={FeedFooter}
ListHeaderComponent={ListHeaderComponent}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
headerOffset={headerOffset}
contentContainerStyle={{
minHeight: Dimensions.get('window').height * 1.5,
}}
style={{paddingTop: headerOffset}}
onScrolledDownChange={onScrolledDownChange}
indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'}
onEndReached={onEndReached}
onEndReachedThreshold={2} // number of posts left to trigger load more
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
extraData={extraData}
// @ts-ignore our .web version only -prf
desktopFixedHeight={
+14 -1
View File
@@ -34,6 +34,7 @@ import {countLines} from 'lib/strings/helpers'
import {useComposerControls} from '#/state/shell/composer'
import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
import {FeedNameText} from '../util/FeedInfoText'
import {useSession} from '#/state/session'
export function FeedItem({
post,
@@ -102,10 +103,14 @@ let FeedItemInner = ({
}): React.ReactNode => {
const {openComposer} = useComposerControls()
const pal = usePalette('default')
const {currentAccount} = useSession()
const href = useMemo(() => {
const urip = new AtUri(post.uri)
return makeProfileLink(post.author, 'post', urip.rkey)
}, [post.uri, post.author])
const isModeratedPost =
moderation.decisions.post.cause?.type === 'label' &&
moderation.decisions.post.cause.label.src !== currentAccount?.did
const replyAuthorDid = useMemo(() => {
if (!record?.reply) {
@@ -284,7 +289,14 @@ let FeedItemInner = ({
postEmbed={post.embed}
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>
</Link>
@@ -364,6 +376,7 @@ const styles = StyleSheet.create({
borderTopWidth: 1,
paddingLeft: 10,
paddingRight: 15,
// @ts-ignore web only -prf
cursor: 'pointer',
overflow: 'hidden',
},
+7 -1
View File
@@ -27,6 +27,7 @@ import {useSession} from '#/state/session'
export function ProfileCard({
testID,
profile: profileUnshadowed,
noModFilter,
noBg,
noBorder,
followers,
@@ -35,6 +36,7 @@ export function ProfileCard({
}: {
testID?: string
profile: AppBskyActorDefs.ProfileViewBasic
noModFilter?: boolean
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
@@ -50,7 +52,11 @@ export function ProfileCard({
return null
}
const moderation = moderateProfile(profile, moderationOpts)
if (moderation.account.filter) {
if (
!noModFilter &&
moderation.account.filter &&
moderation.account.cause?.type !== 'muted'
) {
return null
}
+3 -11
View File
@@ -1,18 +1,16 @@
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 {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
export function ProfileFollowers({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollowers({name}: {name: string}) {
<List
data={followers}
keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
+3 -11
View File
@@ -1,18 +1,16 @@
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 {CenteredView} from '../util/Views'
import {List} from '../util/List'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {usePalette} from 'lib/hooks/usePalette'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
export function ProfileFollows({name}: {name: string}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedDid,
@@ -90,14 +88,8 @@ export function ProfileFollows({name}: {name: string}) {
<List
data={follows}
keyExtractor={item => item.did}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
renderItem={renderItem}
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 pal = usePalette('default')
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>
}
@@ -37,6 +38,7 @@ export function H2({children}: React.PropsWithChildren<{}>) {
const styles = useStyles()
const pal = usePalette('default')
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>
}
@@ -44,6 +46,7 @@ export function H3({children}: React.PropsWithChildren<{}>) {
const styles = useStyles()
const pal = usePalette('default')
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>
}
@@ -51,6 +54,7 @@ export function H4({children}: React.PropsWithChildren<{}>) {
const styles = useStyles()
const pal = usePalette('default')
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>
}
+43 -3
View File
@@ -1,27 +1,42 @@
import React, {memo, startTransition} from 'react'
import {FlatListProps} from 'react-native'
import {FlatListProps, RefreshControl} from 'react-native'
import {FlatList_INTERNAL} from './Views'
import {addStyle} from 'lib/styles'
import {useScrollHandlers} from '#/lib/ScrollContext'
import {runOnJS, useSharedValue} from 'react-native-reanimated'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {usePalette} from '#/lib/hooks/usePalette'
export type ListMethods = FlatList_INTERNAL
export type ListProps<ItemT> = Omit<
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
headerOffset?: number
refreshing?: boolean
onRefresh?: () => void
}
export type ListRef = React.MutableRefObject<FlatList_INTERNAL | null>
const SCROLLED_DOWN_LIMIT = 200
function ListImpl<ItemT>(
{onScrolledDownChange, ...props}: ListProps<ItemT>,
{
onScrolledDownChange,
refreshing,
onRefresh,
headerOffset,
style,
...props
}: ListProps<ItemT>,
ref: React.Ref<ListMethods>,
) {
const isScrolledDown = useSharedValue(false)
const contextScrollHandlers = useScrollHandlers()
const pal = usePalette('default')
function handleScrolledDownChange(didScrollDown: boolean) {
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 (
<FlatList_INTERNAL
{...props}
scrollIndicatorInsets={{right: 1}}
contentOffset={contentOffset}
refreshControl={refreshControl}
onScroll={scrollHandler}
scrollEventThrottle={1}
style={style}
ref={ref}
/>
)
+3 -1
View File
@@ -11,6 +11,7 @@ import {sanitizeHandle} from 'lib/strings/handles'
import {isAndroid} from 'platform/detection'
import {TimeElapsed} from './TimeElapsed'
import {makeProfileLink} from 'lib/routes/links'
import {ModerationUI} from '@atproto/api'
interface PostMetaOpts {
author: {
@@ -23,6 +24,7 @@ interface PostMetaOpts {
postHref: string
timestamp: string
showAvatar?: boolean
avatarModeration?: ModerationUI
avatarSize?: number
displayNameType?: TypographyVariant
displayNameStyle?: StyleProp<TextStyle>
@@ -41,7 +43,7 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
<UserAvatar
avatar={opts.author.avatar}
size={opts.avatarSize || 16}
// TODO moderation
moderation={opts.avatarModeration}
/>
</View>
)}

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