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) ...
@@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 452 KiB |
@@ -42,6 +42,7 @@ module.exports = function (api) {
|
||||
platform: './src/platform',
|
||||
state: './src/state',
|
||||
view: './src/view',
|
||||
crypto: './src/platform/crypto.ts',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 316 B After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 16 KiB |
@@ -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 #}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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,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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] = [
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
@@ -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[]
|
||||
|
||||
@@ -108,6 +108,7 @@ export function transform(legacy: Partial<LegacySchema>): Schema {
|
||||
onboarding: {
|
||||
step: legacy.onboarding?.step || defaults.onboarding.step,
|
||||
},
|
||||
hiddenPosts: defaults.hiddenPosts,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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: [],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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} = {}) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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],
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -174,6 +174,7 @@ export const LoginForm = ({
|
||||
autoCorrect={false}
|
||||
autoComplete="username"
|
||||
returnKeyType="next"
|
||||
textContentType="username"
|
||||
onSubmitEditing={() => {
|
||||
passwordInputRef.current?.focus()
|
||||
}}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -80,6 +80,7 @@ export function Component({image}: Props) {
|
||||
source={{
|
||||
uri: image.cropped?.path ?? image.path,
|
||||
}}
|
||||
contentFit="contain"
|
||||
accessible={true}
|
||||
accessibilityIgnoresInvertColors
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -82,7 +82,7 @@ export function ModalsContainer() {
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalActive) {
|
||||
bottomSheetRef.current?.expand()
|
||||
bottomSheetRef.current?.snapToIndex(0)
|
||||
} else {
|
||||
bottomSheetRef.current?.close()
|
||||
}
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||